wai.delivery.settlement (E6): a hop is paid only for bytes the next hop signed for #404

Merged
dcharlot merged 6 commits from wai/settlement into main 2026-10-21 19:12:00 -04:00
19 changed files with 6166 additions and 17 deletions

View file

@ -126,6 +126,10 @@ on:
# reports, contribution_verify.py reads that table, and the registry's wire identifiers
# test reads its sections.
- 'wai/extensions/contribution.md'
# tests/settlement.rs requires this extension's §11 to list the codes the reference
# reports, settlement_verify.py reads that table, and the registry's wire identifiers test
# reads its sections.
- 'wai/extensions/settlement.md'
- 'wai/wai-web/**'
# The audio provenance check below reads the corpora above with this tool.
- 'wai/tools/wai_audio_provenance_check.py'
@ -798,8 +802,9 @@ jobs:
# e2ee_verify.py (the end-to-end encrypted rooms corpus), live_watermark_verify.py (the
# forensic-watermark corpus), live_ads_verify.py (the live-ads corpus), c2pa_live_verify.py
# (the live-provenance corpus), edge_verify.py (the deriving-edge corpus),
# synthesis_verify.py (the synthesis corpus), ai_usage_verify.py (the AI usage corpus) and
# contribution_verify.py (the contribution-records corpus).
# synthesis_verify.py (the synthesis corpus), ai_usage_verify.py (the AI usage corpus),
# contribution_verify.py (the contribution-records corpus) and settlement_verify.py (the
# delivery settlement corpus).
# Each exits non-zero on any failure or any difference, and the step fails with it.
# One platform: what they check is the reading of the text, not the architecture.
independent-verifiers:
@ -1203,6 +1208,29 @@ jobs:
(cd wai-rs && cargo test --no-default-features --features contribution --lib contribution::)
(cd wai-rs && cargo test --no-default-features --features contribution --test contribution)
python3 -I live-conformance/contribution_verify.py live-conformance/contribution
# wai.delivery.settlement (extensions/settlement.md): the reference module's unit tests
# (range proofs over every range around every chunk boundary of objects of many lengths
# against BLAKE3's own root, tampered proofs refused, the group tree of jwp-receipts §2.2,
# records and item lists read and refused, amounts rounded down per line) and
# tests/settlement.rs (vectors.json regenerates byte for byte from its fixed seeds and no
# other file is there, wai-rs gives every statement and range proof its stated outcome, §11
# lists exactly the reference's codes, each of which the corpus gives, beside every code it
# reuses); then the corpus's second implementation, settlement_verify.py, written from the
# extension's text and sharing no code with wai-rs, which verifies range proofs over
# BLAKE3's chunk tree with claim-conformance's BLAKE3, reads rooms, grants and proofs with
# live_rooms_verify.py and per-hop records with live_metering_verify.py, checks signatures
# with WAI's strict Ed25519 rule, recomputes every figure of every statement, reads the
# extension's reason table, requires every mutated copy of the corpus to fail, and drops
# each of its own checks in turn and requires the corpus to catch the weakened verifier;
# standard library only, run isolated (-I).
- name: delivery settlement · wai-rs and settlement_verify.py
working-directory: wai
shell: bash
run: |
set -euo pipefail
(cd wai-rs && cargo test --no-default-features --features settlement --lib settlement::)
(cd wai-rs && cargo test --no-default-features --features settlement --test settlement)
python3 -I live-conformance/settlement_verify.py live-conformance/settlement
# wai.live.e2ee (extensions/confidentiality.md): the corpus's second implementation,
# e2ee_verify.py, written from the extension's text and the documents it reads and
# sharing no code with the reference (joule-moq's secure_objects and joule-moq-mls,

View file

@ -6,6 +6,50 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### `wai.delivery.settlement`: delivery paid on what the party that received it signed
- **The extension** ([settlement](extensions/settlement.md)) registers the record
`wai.delivery.settlement`. The downstream party of a delivery, a next hop or a sink, signs an
**acknowledgement**, `WDA1`, of what it received in a window: per object class (`live`, `fetch`,
`fill`, `ancillary`), the objects by content hash under jwp-receipts §2.2's tree, the byte range
of each and their byte count, the item lists travelling beside the record under a digest it
signs. A party's acknowledgements to one upstream form a chain of windows that do not overlap.
A relay acknowledges upstream and is acknowledged downstream over the same content hashes, and
a hop's billable bytes are exactly those its downstream parties signed for. An acknowledgement
whose downstream is its upstream is refused (`self_acknowledged`).
- **Unacknowledged delivery.** A hop whose downstream will not or cannot sign states the delivery
in a **declaration**, the same record signed by itself, counted in a class of its own: never as
zero, never billed, and never for a byte the same party acknowledged
(`declared_and_acknowledged`). A unidirectional bearer settles under unidirectional-delivery §4.
- **Byte-range proofs**, `WBR1`: the covered chunks of a range and the chaining value of every
subtree of BLAKE3's chunk tree outside them, verified to the object's content hash; the head
proof of http-delivery §6 is the case of a range that starts at 0. A proof establishes the
object's length only when it reaches the last chunk, and its verifier says whether it did. An
auditor asks a downstream party for one over an item it acknowledged.
- **Paid retrieval.** A fetch below the live edge, a recording fetch, or a fill past a cache is
billed only to a room grant whose `WRP1` proof of possession the request carried, checked by
live-rooms §10.5.4 steps 2 to 5 and the seek power; one proof pays for one record
(`pop_replayed`); an ungranted retrieval is free (`paid_without_grant`).
- **The settlement statement**, `WSS1`, per party and period: lines by object class and payer,
the acknowledged bytes by class, the declared bytes, the party's joules by acquisition class
from its per-hop records (`WMH1`) and never one total across classes (`classes_summed`), and the
amount under a declared tariff, in millionths of a minor unit per 10^9 bytes, rounded down once
per line. A verifier recomputes every figure from the acknowledgements: a line billing bytes no
acknowledgement covers is `bytes_not_acknowledged`, any other difference `statement_mismatch`.
A line kind is reserved for a payee that is not a hop.
- **Logs.** Each acknowledged item of an HTTP delivery maps onto a `cdni_http_request_v1` record
(RFC 7937 §3.4.1), the acknowledgement's digest in a `remark` directive; a log carries no
counter-signature, so a converter maps a log onto declarations, never onto acknowledgements.
- **Viewers as relays over ciphertext.** An acknowledgement binds a sealed object's content hash,
over its sealed bytes, so a forwarder that cannot open what it forwards earns acknowledged bytes,
and proves ranges of them, on the terms of any relay.
- **Reference and corpus.** `wai-rs` module `settlement` (feature `settlement`), its generator
`wai_settlement_vectors` and `tests/settlement.rs`; the corpus
[`live-conformance/settlement/`](live-conformance/settlement/), 48 statements and 24 range
proofs; and its second implementation, `live-conformance/settlement_verify.py`, written from the
text, which requires every mutated copy of the corpus to fail and every one of its own checks
to be load-bearing.
### `wai.media.contribution`: who contributed what, computed from the process
- **The extension** ([contribution](extensions/contribution.md)) registers the record

View file

@ -46,7 +46,8 @@ Every registered string has one kind.
`wai.text.timed`; the deriving edge's record, `wai.edge.derivation`; the origin claims of
media, `wai.media.synthesis`; AI usage preferences and consumption claims, `wai.ai.usage`;
the contribution records of media made by people and models together,
`wai.media.contribution`). It names no payload format.
`wai.media.contribution`; delivery settled on what its receiver signed,
`wai.delivery.settlement`). It names no payload format.
A derivation or record string is not a payload capability, so it is not
dispatchable (SPEC §4): dispatch never takes an envelope's capability that names
@ -177,6 +178,7 @@ and a role are fixed once registered.
| `wai.audio.score` | payload | registered | audio | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content |
| `wai.avatar.pose` | payload | registered | interactive | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content |
| `wai.capture.take` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.delivery.settlement` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content |
| `wai.derive.crop` | derivation | registered | — | — | patent-pool | — | — | §5 Prior artifacts, general tensors and derivation operations |
| `wai.derive.dissolve` | derivation | registered | — | — | patent-pool | — | — | §5 Prior artifacts, general tensors and derivation operations |
| `wai.derive.grid_composition` | derivation | registered | — | — | patent-pool | — | — | §5 Prior artifacts, general tensors and derivation operations |
@ -320,10 +322,12 @@ and a role are fixed once registered.
| `WAI_RECEIPT` | catalog | defined | moq-streaming-format §5 |
| `WAP1` | magic | defined | live-ads §3.1 |
| `WAU1` | magic | defined | ai-usage §4.2 |
| `WBR1` | magic | defined | settlement §5.2 |
| `WCD1` | magic | defined | live-compose §3.1 |
| `WCL1` | magic | defined | live-recording §5.1 |
| `WCN1` | magic | defined | capture-binding §8.1 |
| `WCR1` | magic | defined | live-compose §8.1 |
| `WDA1` | magic | defined | settlement §3.1 |
| `WEP1` | magic | defined | live-events §5 |
| `WEV1` | magic | defined | live-events §3.1 |
| `WHP1` | magic | defined | http-delivery §6.2 |
@ -341,6 +345,7 @@ and a role are fixed once registered.
| `WSD1` | magic | defined | energy-steering §7.1 |
| `WSIX` | magic | defined | packaging §6 |
| `WSP1` | magic | defined | energy-steering §3.1 |
| `WSS1` | magic | defined | settlement §7.1 |
| `WTM1` | magic | defined | capture-binding §7.1 |
| `WTX1` | magic | defined | timed-text §4.2 |
| `WUR1` | magic | defined | capture-binding §6.2 |

View file

@ -596,9 +596,10 @@ kind:
`wai.live.metering`, `wai.live.steering`, `wai.live.moderation`, `wai.live.e2ee`,
`wai.live.watermark`, `wai.live.ads` and `wai.live.provenance`, and timed text,
`wai.text.timed`; an edge's derivation record, `wai.edge.derivation`; the origin claims of
media, `wai.media.synthesis`; AI usage preferences and consumption claims, `wai.ai.usage`; and
media, `wai.media.synthesis`; AI usage preferences and consumption claims, `wai.ai.usage`;
the contribution records of media made by people and models together,
`wai.media.contribution`), which names no payload format.
`wai.media.contribution`; and delivery settled on what its receiver signed,
`wai.delivery.settlement`), which names no payload format.
Only payload, companion and refinement capabilities have a media class, a
fidelity tier, a replicate class and a role. A derivation or record string is not a payload
@ -1183,6 +1184,26 @@ counts per party, and a sample no disclosed, pinned rule fixes is `unknown` or `
never credited to anyone. WAI decides no legal question: the record states facts a reader applies
its own rule to: [`extensions/contribution.md`](extensions/contribution.md).
And for **settling delivery** on what the party that received it signed — `wai.delivery.settlement`:
the downstream party of a delivery, a next hop or a sink, signs an acknowledgement, `WDA1`, of what it
received in a window: per object class (live, fetch, fill, ancillary), the objects by content hash
under jwp-receipts §2.2's tree, the byte range of each, and their byte count, each acknowledgement
chained to the one before it for the same upstream. A relay acknowledges upstream and is
acknowledged downstream over the same content hashes, and a hop's billable bytes are exactly the
bytes its downstream parties signed for. A hop whose downstream does not sign declares that
delivery in the same record signed by itself, counted apart as unacknowledged: never as zero, never
as acknowledged; a unidirectional bearer settles under unidirectional-delivery §4. A range proof,
`WBR1`, shows that given bytes are a range of an object over BLAKE3's chunk tree without the rest of
it, and answers an audit of an acknowledged item. A retrieval that costs money (a fetch below the
live edge, a recording fetch, a fill past a cache) is billed only to a room grant whose proof of
possession, `WRP1`, the request carried; an ungranted retrieval is free. Per party and per period, a
settlement statement, `WSS1`, states the acknowledged bytes by class and payer, the declared bytes,
the party's joules by acquisition class from its per-hop records, never summed across classes, and
the amount under a declared tariff in integer micro-units; a verifier recomputes every figure from
the acknowledgements and refuses a statement that does not recompute. Acknowledgements map onto CDNI
logging records (RFC 7937), whose fields carry no counter-signature, and a line kind is reserved for
a payee that is not a hop: [`extensions/settlement.md`](extensions/settlement.md).
### Payloads of the WAI-native integer codecs
This revision pins a payload format to each WAI-native integer codec whose row named

View file

@ -0,0 +1,735 @@
# WAI Extension: Delivery Settlement
> Status: Draft. Registers the record `wai.delivery.settlement`: delivery paid on what the
> party that received it signed, not on what the party that sent it logged. The party
> downstream of a delivery, a next hop or a sink, signs an **acknowledgement**, `WDA1`, of what
> it received in a window: per object class, the objects by content hash under
> [jwp-receipts](jwp-receipts.md) §2.2's tree, the byte ranges of each, and their byte count. A
> relay is the downstream party of the acknowledgement it signs upstream and the upstream party
> of those its downstream sign for it, and both name the objects by the same content hashes. A
> hop whose downstream will not or cannot sign states that delivery in a **declaration**, the
> same record signed by the hop itself, which is counted in a class of its own,
> `unacknowledged`: never as zero and never as acknowledged. A **byte-range proof**, `WBR1`,
> shows that given bytes are a given range of an object without the rest of it, over BLAKE3's
> chunk tree. A retrieval that costs money (a fetch below the live edge, a recording fetch, an
> origin fill past a cache) is billed only to a grant whose holder's proof of possession the
> request carried ([live-rooms](live-rooms.md) §10.5); an ungranted retrieval is free. Per
> party and per period, a **settlement statement**, `WSS1`, states the acknowledged bytes by
> object class, the declared bytes apart, the party's energy by acquisition class from its
> per-hop records ([live-metering](live-metering.md) §3), never summed across classes, and the
> amount under a declared tariff in integer micro-units; a verifier recomputes every figure
> from the acknowledgements, and refuses a statement that does not recompute. No new
> transport, no payment rail and no money. The reference implementation is the `settlement`
> module of `wai-rs` (feature `settlement`); the corpus is
> [`live-conformance/settlement/`](../live-conformance/settlement/), and its second
> implementation is `live-conformance/settlement_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: the WAI texts this one cites; RFC 7937 (§3.2 to §3.4.1, the
> logging file, its directives and the `cdni_http_request_v1` record); RFC 9246 (§4.5, the
> logging fields of URI Signing, and §6.2.1, the record type `cdni_http_request_v2`); the
> BLAKE3 specification (its chunk chaining values, parent nodes and ROOT flag); RFC 6962
> §2.1; RFC 8032; FIPS 180-4. Of SVTA2050, SVTA2007 and SVTA2006 only their published
> summaries were read; §9.3 says what follows from that.
## 1. Scope
In scope: the acknowledgement, its entries, its object classes, its items, its signature, its
rules and its chains (§3); the declaration of a delivery nobody acknowledged (§4); the
byte-range proof and the audit it answers (§5); who may trigger a retrieval that costs money,
and how an acknowledgement records it (§6); the settlement statement, its lines, its tariff,
its energy and its recomputation (§7); verification (§8); the mapping onto CDNI and open
caching logs (§9); and a viewer that relays objects it cannot read (§10).
Out of scope: a transport; money, a payment rail, an invoice and any contact with a party
outside the records; how a party chooses its tariff; how a hop measures its energy, which
[energy-measurement](energy-measurement.md) governs; the delivery receipts themselves, which
[jwp-receipts](jwp-receipts.md) defines and which a downstream party checks before it
acknowledges anything; and payment to a party that is not a hop, for which §7.2 keeps a line
kind and this revision defines none.
Relationship to the core spec: `wai.delivery.settlement` is a **record** ([SPEC](../SPEC.md)
§5): a signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
## 2. Terms
- **Party**: an Ed25519 key (RFC 8032) that delivers or receives objects: a **hop** (a relay, a
cache, an origin, a compositor, an ingest gateway, a deriving edge) or a **sink** (a viewer,
a player, an agent).
- **Upstream** and **downstream**: of one delivery, the party that sent the bytes and the party
that received them.
- **Object**: one WAI envelope as it travels, sealed bytes for a sealed object
([confidentiality](confidentiality.md) §8.1). Its **content hash** is jwp-receipts §2.1's,
`BLAKE3("moq-jwp:object\x01" ‖ envelope)`, and its **length** is the envelope's length in
bytes.
- **Time**: microseconds since 1970-01-01T00:00:00Z, the unit of a room's `t0`
(live-rooms §3.1). A **window** and a **period** are spans of time, each from its `start`
(included) to its `end` (excluded).
- **Tree root** of a list of content hashes: jwp-receipts §2.2's root in its first (unlabelled)
form: each leaf `BLAKE3("moq-jwp:merkle-leaf\x01" ‖ content_hash)`, each node
`BLAKE3("moq-jwp:merkle-node\x01" ‖ left ‖ right)`, the last node of an odd level paired with
itself, a single leaf its own root, and 32 zero bytes for no leaf.
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers of that many bits. A **digest** is
SHA-256 (FIPS 180-4), 32 bytes. **Zero** of a key or digest field is 32 zero bytes.
## 3. The acknowledgement
### 3.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WDA1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `mode` | 1 | 1 an **acknowledgement**, signed by `downstream`; 2 a **declaration** (§4), signed by `upstream` |
| `role` | 1 | the downstream party's role: 1 a hop, 2 a sink |
| `upstream` | 32 | the key of the party that delivered |
| `downstream` | 32 | the key of the party that received; in a declaration, zero for a downstream party with no key the upstream knows |
| `start`, `end` | 8 each | u64: the window the record covers |
| `prev` | 32 | the digest of the record before it in its chain (§3.7); zero for the first |
| `entries` | 2 + 141 × *n* | a u16 count *n*, then each entry (§3.2) |
| `sig` | 64 | §3.5 |
### 3.2 Entries
An entry states what the downstream party received in the window of one object class, under
one grant and one proof:
| field | size | meaning |
|---|---|---|
| `class` | 1 | the object class (§3.3) |
| `grant` | 32 | zero, or the digest of a room grant (live-rooms §4.2): for a delivery class the grant the downstream party received under, for a retrieval class the grant the retrieval is billed to (§6) |
| `proof` | 32 | zero, or the SHA-256 of the proof record (`WRP1`, live-rooms §10.5.1) the retrieval's request carried (§6) |
| `objects` | 4 | u32: the items of the entry (§3.4) |
| `bytes` | 8 | u64: the sum of its items' byte counts |
| `root` | 32 | the tree root of its items' content hashes, in the items' order |
| `items` | 32 | SHA-256 over the 17 bytes `wai:settle-items` (ASCII) and 0x01, followed by its item list (§3.4) |
### 3.3 Object classes
| class | name | what it counts |
|---|---|---|
| 1 | `live` | media objects delivered to a subscription at or after the live edge, as they were published |
| 2 | `fetch` | objects served from a store in answer to a request that seeks back below the live edge (live-rooms §10.6) or that fetches a recording ([live-recording](live-recording.md) §8) |
| 3 | `fill` | objects a cache or a relay retrieved from its upstream to answer a request it could not answer from what it held |
| 4 | `ancillary` | events, catalogs, receipts tracks, timed text and every other object that is not media |
Classes 1 and 4 are **delivery classes**; classes 2 and 3 are **retrieval classes**, which cost
the serving party a store or an upstream fetch and are billed only to a grant (§6). An entry of
class 3 is signed by the cache or relay that filled, for its upstream, and is billed to the
grant of the request it was filling for.
### 3.4 Items
An entry's **item list** is the concatenation of its items, each 48 bytes:
| field | size | meaning |
|---|---|---|
| `content_hash` | 32 | the object's content hash |
| `start`, `end` | 8 each | u64: the byte range of the object's envelope the downstream party received; a whole object is `start` 0 and `end` its length |
An item's **byte count** is `end` − `start`. An object received in parts (an HTTP range
request, a fill that stopped) is acknowledged by the ranges received. A downstream party lists
the items of a whole group in the group's order, so that its entry's root is the root the
group's publisher signed (jwp-receipts §2.3) when the entry holds exactly that group, whole and
unlabelled. The item lists travel beside the record, unsigned; the record signs each list's
digest and the root over its content hashes, so the list can only be the one its signer signed,
and one object's place in an entry can be shown by its jwp-receipts §2.4.2 inclusion proof
against `root`, without the other items.
### 3.5 Digest and signature
The **digest** of a record is SHA-256 over its bytes before `sig`. Its **signature** is Ed25519
over the 15 bytes `wai:settle-ack` (ASCII) and 0x01, followed by the bytes the digest covers,
by `downstream` in an acknowledgement and by `upstream` in a declaration, verified strictly as
[SPEC](../SPEC.md) §7 *Signatures* requires: a key that fails step 1 or 2 is `bad_key`, and a
signature that fails a later step is `bad_signature`.
### 3.6 Reading and rules
A reader reads exactly the bytes it is given as one record, in the order of §3.1, and refuses
at the first of these that holds: fewer than 4 bytes, `truncated`; a magic other than `WDA1`,
`malformed`; no fifth byte, `truncated`; a version other than 1, `unknown_version`; the bytes
end before a field the layout requires, `truncated`; bytes after `sig`, `malformed`. It then
refuses as `ack_invalid` a record that breaks any of these:
- `mode` is 1 or 2, `role` is 1 or 2, and `start` is below `end`;
- `upstream` is not zero, and in an acknowledgement `downstream` is not zero;
- there is at least one entry, and the entries are in strictly ascending order of (`class`,
`grant`, `proof`), each `class` 1 to 4;
- each entry's `objects` is at least 1 and its `bytes` at least its `objects`;
- an entry of a delivery class has a zero `proof`; an entry of a retrieval class has `grant`
and `proof` both zero or both not zero.
An acknowledgement whose `downstream` is its `upstream` is refused `self_acknowledged`: a party's
statement that it received what it delivered is not an acknowledgement, and a hop states its
own delivery only in a declaration (§4). Then the signature (§3.5): `bad_key`, `bad_signature`.
An item list is read as items of 48 bytes, its length a multiple of 48, each item's `start`
below its `end`, and no two items sharing a byte of one object (the same content hash, with
ranges that overlap): otherwise `items_invalid`. It
**recomputes** against its entry when it holds `objects` items, the sum of their byte counts is
`bytes`, the tree root of their content hashes in order is `root`, and its digest is `items`:
otherwise `items_mismatch`. The sum is taken in integers wide enough not to overflow.
### 3.7 Chains
A **chain** is the records of one `mode`, one `upstream` and one `downstream`. Its records
cover windows that do not overlap, and each names in `prev` the digest of the record of its
chain whose window comes before its own; the first record of a chain names zero. So a party's
acknowledgements to one upstream are a sequence, and an acknowledgement signed for one window
cannot stand for another: the window is signed, and so is its place in the chain.
**Through a relay.** A relay is the downstream party of the acknowledgement it signs for its
upstream, and the upstream party of the acknowledgements its own downstream parties sign for
it. A relay forwards envelopes unaltered (jwp-receipts §4.1), so the two name the objects by the
same content hashes: what a relay is paid for can be matched, object by object, against what it
acknowledged receiving. A deriving edge ([deriving-edge](deriving-edge.md)) is paid for objects
of its own, which its derivation records name with the inputs it acknowledged. A hop's
**billable bytes** are exactly the bytes its downstream parties signed for: the sum of the
`bytes` of the entries of the acknowledgements naming it as `upstream` (§7.6).
## 4. Unacknowledged delivery
### 4.1 Declarations
A downstream party may not sign: a player that holds no key, a viewer admitted by Privacy Pass
(live-rooms §10.4), a sink that will not, or a link with no return path. The hop that delivered
to it states that delivery in a **declaration**: a record of §3's layout with `mode` 2, signed
by the hop, naming the downstream party's key where it has one and zero otherwise. A
declaration's entries and items keep §3's rules. Deliveries to downstream parties with no key
form one chain per hop, so in one window they are stated together, in one declaration.
### 4.2 What a declaration is
A declaration is the delivering party's own statement. Its bytes are counted in a class of
their own, **unacknowledged**:
- **never zero**: an entry states at least one byte, so a declaration says what was delivered,
and a statement states the declared bytes beside the acknowledged ones (§7.1);
- **never billable as acknowledged**: no line of a statement is computed from a declaration
(§7.6), and a deployment that pays for declared delivery does so outside the statement;
- **never both**: no byte of an object is both acknowledged and declared for one downstream
party in windows that overlap. A declaration naming a downstream key whose items share a byte
of an object with the items of an acknowledgement by that key, for the same upstream, in a
window that overlaps the declaration's, is refused `declared_and_acknowledged` (§8.2 step 4).
### 4.3 Bearers with no return path
A unidirectional bearer settles under unidirectional-delivery §4: what a transmitter sent over
it is declared, never acknowledged, and a sink's later reconciliation is reported apart from
the chain as that section requires. This extension adds nothing for it.
## 5. Byte-range proofs
### 5.1 The tree
An object's content hash is the BLAKE3 root, in hash mode, over 1024-byte chunks of
`X = "moq-jwp:object\x01" ‖ envelope`, the chunk at byte offset 1024 × *i* of `X` having chunk
counter *i*. With *L* = 15 + the object's length, `left(n) = 1024 · 2^⌊log₂((n − 1)/1024)⌋`
for *n* above 1024 splits a subtree of *n* bytes into its left and right children
([http-delivery](http-delivery.md) §6.3, whose head proof is the case of a range that starts
at 0). A **range proof** shows that the bytes it carries are the bytes at given offsets of the
object with a given content hash: it carries the chunks that hold the range, and the chaining
value of every subtree of `X` that lies wholly outside them.
### 5.2 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WBR1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `content_hash` | 32 | the object's content hash |
| `envelope_len` | 8 | u64: the object's length |
| `start`, `end` | 8 each | u64: the byte range of the envelope it proves |
| `n_cv` | 1 | u8: the chaining values it carries |
| `cvs` | 32 × `n_cv` | the chaining values, in the order §5.4 consumes them |
| `bytes` | the rest | the covered bytes (below) |
With *c*<sub>0</sub> = 1024 · ⌊(15 + `start`) / 1024⌋ and *c*<sub>1</sub> = min(*L*, 1024 ·
⌈(15 + `end`) / 1024⌉), the **covered chunks** are the bytes of `X` from *c*<sub>0</sub> to
*c*<sub>1</sub>, and `bytes` is the part of them that is envelope: the envelope's bytes from
max(*c*<sub>0</sub>, 15) − 15 to *c*<sub>1</sub> − 15. A verifier knows the 15 bytes of the
domain and never reads them from the record.
### 5.3 Reading
A reader refuses at the first of these that holds: fewer than 4 bytes, `truncated`; a magic
other than `WBR1`, `malformed`; no fifth byte, `truncated`; a version other than 1,
`unknown_version`; fewer than 62 bytes, `truncated`; `start` not below `end`, `range_invalid`;
`end` above `envelope_len`, or *L* past what a u64 holds, `range_outside_object`; fewer bytes
than 62 + 32 × `n_cv` + the covered length (`bytes` above), `truncated`; more, `malformed`.
### 5.4 Verification
A range proof is checked against the content hash *H* of the object it is offered for. With
the covered chunks of `X` held, and in order:
1. its `content_hash` is *H*: otherwise `range_proof_mismatch`;
2. where it answers an audit (§5.6), its range is within the audited item's: otherwise
`range_not_acknowledged`;
3. the root computes to *H*, consuming its chaining values exactly: otherwise
`range_proof_mismatch`. When *c*<sub>0</sub> is 0 and *c*<sub>1</sub> is *L*, the covered
chunks are the whole of `X`: `n_cv` is 0 and the root is BLAKE3 of `X`. Otherwise
```
cv(s, n):
if s + n ≤ c0 or s ≥ c1: take the next chaining value from cvs
elif c0 ≤ s and s + n ≤ c1: the non-root chaining value of the n bytes of X at s,
a subtree whose first chunk has counter s / 1024
else: parent_cv(cv(s, left(n)), cv(s + left(n), n − left(n)))
root = parent_root(cv(0, left(L)), cv(left(L), L − left(L)))
```
`parent_cv` and `parent_root` are BLAKE3's parent node in hash mode, without and with the ROOT
flag. *c*<sub>0</sub> is a multiple of 1024, and so is *c*<sub>1</sub> unless it is *L*, so a
subtree the procedure meets lies wholly inside the covered chunks or wholly outside them once it
is split far enough; a single chunk is never split. A chaining value left over, or one missing,
is `range_proof_mismatch`.
### 5.5 What a proof establishes
A proof that passes establishes that the bytes from `start` to `end` it carries are those bytes
of the envelope whose content hash is *H*. The root does not encode the object's length, so
`envelope_len` is established only when *c*<sub>1</sub> is *L*, when the proof's last covered
chunk is the object's last: a **tail proof**. A proof of a range that ends before the last chunk
establishes the tree's shape and not the length (as http-delivery §6.3 check 3 notes of a head
proof), and a verifier reports whether the length was established. A proof is a pure function
of the object and the range: anyone holding the object can make one, and a proof over bytes
that are not the object's at those offsets fails step 3.
### 5.6 Audits
An auditor (the party paid, the party paying, or a party settling between them) may name an
item of an acknowledgement and a range within it, and ask the downstream party that signed it
for a range proof. The audit passes when the proof verifies against the item's content hash
(§5.4) and its range is within the item's; a downstream party that cannot answer has not shown
that it holds the bytes it signed for. A proof carries the covered chunks, so an audit costs the
range rounded out to whole chunks and two chaining values per level of the tree, not the
object: a few chunks drawn at random audit a large object. Over a sealed object the proof is
over the sealed bytes, which a party that cannot open them can prove as well as one that can
(§10).
## 6. Paid retrieval
### 6.1 The rule
A hop **MUST NOT** perform a retrieval of a retrieval class (§3.3) that costs money (a fetch
from its store below the live edge or of a recording, a fill from its upstream past what it
holds) on a request that carries no grant and no proof of possession of it (live-rooms §10.5).
An ungranted request is served free from what the hop holds, or refused, as the hop's policy
says: a relay that seeks back refuses it (live-rooms §10.6, `seek_not_granted`), and an HTTP
store may serve a preview free. A retrieval performed on an ungranted request, a fill among
them, is the serving hop's own cost and is never billed to a grant holder.
### 6.2 Recording the grant
The acknowledgement of a retrieval names, in its entry, the grant the retrieval is billed to and
the proof the request carried: `grant` the grant's digest, `proof` the SHA-256 of the `WRP1`
record. Over MOQT the proof is the session's `GRANT_PROOF` (live-rooms §10.5.2), one for every
retrieval of the session; over HTTP it is the request's `WAI-Room-Proof` (live-rooms §10.5.3).
A sink that fetched names its own grant and proof. A cache that filled for a request names the
grant and the proof that request carried, and hands the grant and the proof records to its
upstream beside its acknowledgement. An entry of a retrieval class with a zero `grant` records
a retrieval no grant was presented for: it is acknowledged, and free (§7.6).
### 6.3 Checking
For each entry naming a grant, a verifier checks, in order:
1. the grant is among the grants of a room it is given (§8.1): otherwise `record_missing`;
2. the grant is good throughout the record's window, `nbf` × 10^6 ≤ `start` and `end` ≤ `exp` ×
10^6: otherwise `outside_grant_window`;
3. for a delivery class, the grant's `principal` is the record's `downstream`: otherwise
`grant_wrong_principal`;
4. for a retrieval class, the proof whose SHA-256 is `proof` is given (`record_missing`), and
it passes live-rooms §10.5.4 steps 2 to 5 for the grant: it reads (`pop_malformed`), names
the grant (`pop_wrong_grant`), its `key` is the grant's `principal` (`pop_wrong_key`), and
its signature verifies (`pop_bad_signature`);
5. for class 2, the grant holds the seek power (live-rooms §3.3, bit 4): otherwise
`seek_not_granted`.
Steps 6 and 7 of live-rooms §10.5.4, the challenge and the context, were the serving hop's when
the request arrived: a verifier of a statement holds neither the challenge nor the request.
What it holds instead is the rule that one presentation pays for what one session or one
request retrieved: a proof named by entries of two records is refused `pop_replayed` (§8.2
step 4). Revocation through the room authority's key log (live-rooms §5) was the serving hop's
to check at the request, and is not checked again.
## 7. The settlement statement
### 7.1 Layout
Per party and per period, the party signs one statement of what it is owed:
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WSS1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `party` | 32 | the key the statement settles, which signs it |
| `start`, `end` | 8 each | u64: the period |
| `records` | 2 + 32 × *n* | a u16 count, then the digests of the acknowledgements and declarations naming `party` as `upstream` that it settles, in strictly ascending order |
| `hops` | 2 + 32 × *k* | a u16 count, then the digests of the party's per-hop records (live-metering §3), in strictly ascending order |
| `tariff` | 3 + 32 | `currency` (three capitals `A`–`Z`, ISO 4217; three zero bytes for no tariff), then four u64 prices, one per object class in class order, each in millionths of a minor unit of `currency` per 10^9 bytes |
| `classes` | 64 | for each object class in order, two u64: the acknowledged `objects` and `bytes` |
| `unacknowledged` | 20 | `declarations` (u32), then `objects` and `bytes` (u64 each): the declared delivery (§4) |
| `unmetered` | 2 | u16: the hop records whose figure is the unmetered marker |
| `totals` | 1 + 13 × *m* | a u8 count, then for each acquisition class among the metered hop records: `tag` (u8), `figures` (u32) and `joules` (u64, µJ), in strictly ascending order of `tag` |
| `lines` | 2 + 90 × *p* | a u16 count, then each line (§7.2) |
| `amount` | 8 | u64: the sum of the lines' `amount`, in millionths of a minor unit |
| `sig` | 64 | §7.4 |
### 7.2 Lines
A line is 90 bytes:
| field | size | meaning |
|---|---|---|
| `kind` | 1 | 1 a delivery line; 2 is reserved (below) |
| `payee` | 32 | the key paid |
| `class` | 1 | the object class |
| `payer` | 32 | who the line is billed to (below) |
| `objects` | 8 | u64: the acknowledged objects |
| `bytes` | 8 | u64: their acknowledged bytes |
| `amount` | 8 | u64: the line's amount, in millionths of a minor unit |
A **delivery line** (kind 1) has `payee` the statement's `party`, and states, for one object
class and one payer, the objects and bytes of the entries of the statement's acknowledgements
of that class billed to that payer. The **payer** of an entry of a delivery class is its
record's `downstream`, the party that received; of an entry of a retrieval class, its `grant`,
the grant the retrieval is billed to, or zero for a retrieval no grant was presented for. A line
of a retrieval class whose payer is zero is **free**.
Kind 2 is reserved for a line whose payee is not a hop: a later revision's payment to the
creators of an output, from the attribution of what conditioned it. Such a line names its payee
in `payee` and its basis in `payer`, so a statement can carry it beside the delivery lines with
no change to the layout. This revision defines no kind 2 line, and a reader refuses one
(§7.5).
### 7.3 Tariff and amounts
The tariff gives each object class a price in millionths of a minor unit of `currency` per 10^9
bytes. A line's **amount** is ⌊`bytes` × *p* / 10^9⌋ with *p* its class's price, computed in
integers wide enough not to overflow, and 0 for a free line. The statement's `amount` is the
sum of its lines' amounts. With no tariff every price is 0 and so is every amount. The smallest
price, 1, is one thousandth of a minor unit per 10^12 bytes. An amount is rounded down once per
line, so a statement never states more than its bytes at its prices.
### 7.4 Digest and signature
The **digest** of a statement is SHA-256 over its bytes before `sig`; its **signature** is
Ed25519 by `party` over the 21 bytes `wai:settle-statement` (ASCII) and 0x01, followed by the
bytes the digest covers, verified strictly (SPEC §7 *Signatures*).
### 7.5 Reading and rules
A reader reads exactly the bytes it is given as one statement, as §3.6 reads a record with
`WSS1` for `WDA1` (`truncated`, `malformed`, `unknown_version`), and refuses as
`classes_summed` a statement whose `totals` names tag 0: a total over figures of no one class
(live-metering §4.2). It refuses as `statement_invalid` a statement that breaks any of these:
- `start` is below `end`; `records` holds at least one digest; `records` and `hops` are in
strictly ascending order;
- with no tariff every price is 0; otherwise `currency` is three capitals `A`–`Z`;
- each total's `tag` is 2 to 5, its `figures` above 0, and the totals in strictly ascending
order of `tag`;
- each line's `kind` is 1, its `payee` the `party`, its `class` 1 to 4 and its `objects` above
0; the lines are in strictly ascending order of (`kind`, `payee`, `class`, `payer`).
Then the signature (§7.4): `bad_key`, `bad_signature`.
### 7.6 Recomputation
Every figure of a statement is recomputed from the records it names:
- the **lines**: for each object class and payer among the entries of its acknowledgements
(mode 1), one line, with the sums of those entries' `objects` and `bytes` and the amount of
§7.3. No line is computed from a declaration;
- `classes`: for each object class, the sums of its lines' `objects` and `bytes`;
- `unacknowledged`: the number of its declarations (mode 2), and the sums of their entries'
`objects` and `bytes`;
- `unmetered` and `totals`: from its hop records, as live-metering §4.5 computes a receipt's
from its hop entries (§7.7);
- `amount`: the sum of the lines' amounts.
A verifier compares in this order and refuses at the first difference:
1. a free line whose `amount` is not 0: `paid_without_grant`;
2. a line whose class and payer no entry gives, or that states more objects or more bytes than
its entries give: `bytes_not_acknowledged`, the bytes a statement bills that no
acknowledgement covers;
3. a total that live-metering §4.5 calls summed across classes: `classes_summed`;
4. any other difference, a line missing, fewer bytes than acknowledged, an amount, a class, the
declared figures, `unmetered`, a total: `statement_mismatch`.
A figure that cannot be held in its field is a difference.
### 7.7 Energy
A statement states the party's energy over the period from its per-hop records (live-metering
§3) as live-metering §4.5 states a session's: `unmetered` counts the records whose figure is
the unmetered marker, which are in no total and are never presented as zero joules; each total
is one acquisition class's number of figures and their sum; and nothing sums the figures of two
classes. A record counts when its `hop` is the `party` and its span, placed in time by its
room's `t0`, lies within the period: `t0` + its `start` ≥ the period's `start` and `t0` + its
`end` ≤ the period's `end`. The energy is **whole** when `unmetered` is 0 and at least one
record is named, **partial** when a named record is unmetered, and **none** when no record is
named. Joules are not divided among lines or bytes in this revision: an energy figure says what
the party spent, and a line says what it is owed.
## 8. Verification
### 8.1 Inputs
A verifier is given the statement; the records it names, each with the item list of each of its
entries; the proof records (`WRP1`) its entries name; the per-hop records it names; and the
**rooms** the grants and hop records come from, each a room descriptor (live-rooms §3) and the
room's grants in order (live-rooms §4.3).
### 8.2 Steps
It runs these steps and stops at the first refusal, which it reports with the reason and where:
1. **The statement** (§7.5), at `statement`: `truncated`, `malformed`, `unknown_version`,
`classes_summed`, `statement_invalid`, `bad_key`, `bad_signature`.
2. **The rooms**, at `room` and the room id: each descriptor reads and verifies (live-rooms §3.4,
with its reasons), and its grants check (live-rooms §4.3, with its reasons, at `grant` and
the grant's index).
3. **Each record**, in the statement's order, at `record` and its index:
1. a record whose digest it names is given: otherwise `record_missing`;
2. it reads and verifies (§3.6): `truncated`, `malformed`, `unknown_version`, `ack_invalid`,
`self_acknowledged`, `bad_key`, `bad_signature`;
3. its `upstream` is the statement's `party`: otherwise `ack_wrong_party`;
4. its window is within the period, `start` ≤ its `start` and its `end` ≤ `end`: otherwise
`ack_outside_period`, an acknowledgement replayed from another period among them;
5. each entry, in order, at `entry` and its index: its item list is given
(`record_missing`), reads (`items_invalid`) and recomputes (`items_mismatch`); and, where
it names a grant, §6.3 steps 1 to 5.
4. **The chains**, at `record` and an index. Order each chain's records (§3.7) in the statement
by (`start`, index). Then, for each record in the statement's order that has a record *p*
before it in that order, *p* the last such: its window overlaps *p*'s (*p*'s `end` is above
its `start`), `ack_overlap`; or its `prev` is not *p*'s digest, `ack_chain_broken`. Then, in
the statement's order, each declaration that breaks §4.2's last rule is
`declared_and_acknowledged`. Then, in the statement's order of records and each record's
order of entries, an entry naming a proof that an entry of an earlier record names is
`pop_replayed`, at `entry` and its index.
5. **Each hop record**, in the statement's order, at `hop` and its index: a record whose digest
it names is given (`record_missing`); it reads as live-metering §3.6 reads a record of the
room its `room` field names, with that section's reasons; that room is given
(`record_missing`); its `hop` is the `party` (`hop_not_party`); and its span is within the
period (§7.7, `span_outside_period`).
6. **The figures** (§7.6), at `statement`: `paid_without_grant`, `bytes_not_acknowledged`,
`classes_summed`, `statement_mismatch`.
A verifier finds a record, a hop record or a proof among the bytes it is given by digest: the
digest of given bytes, of a record, a hop record and a descriptor alike, is SHA-256 over all but
their last 64 bytes, and of a proof record SHA-256 over all of it. A room that does not read is
named by the digest of its descriptor's bytes.
### 8.3 Outcome
- **refuse**: the reason and where.
- **accept**: the statement's digest; its currency; the acknowledged objects and bytes by object
class; each line with its amount and whether it is free; the declared figures; the energy's
coverage, `unmetered` and the totals by class label; and the amount. Every figure is what the
acknowledgements, the declarations and the hop records give.
A range proof's verifier (§5.4) reports **refuse** with its reason, or **accept** with the
range, `envelope_len`, the covered chunks, and whether the length was established (§5.5).
## 9. Logs
### 9.1 CDNI logging records
A CDN that settles under this extension and exchanges logs under RFC 7937 maps each item of an
acknowledgement of an HTTP delivery onto one `cdni_http_request_v1` record (RFC 7937 §3.4.1),
or `cdni_http_request_v2` where it uses URI Signing (RFC 9246 §6.2.1):
| RFC 7937 field | what it carries here |
|---|---|
| `date`, `time`, `time-taken` | when the surrogate finished the request; the record's window holds that instant |
| `cs-method`, `protocol`, `sc-status` | the request; `sc-status` 200 for an item of a whole object, 206 for a range, whose `Content-Range` gives the item's `start` and `end` |
| `cs-uri`, `u-uri` | the object's URI; under http-delivery §5.2 it is `<cid>.wai`, and the cid is the item's `content_hash` |
| `sc-entity-bytes` | the item's byte count |
| `sc-total-bytes` | the response's bytes with its status line and headers, which an acknowledgement never counts |
| `s-cached` | 1 for an item of class 1, 2 or 4 served from what the surrogate held; 0 when it filled, whose fill is a class 3 entry of the surrogate's own acknowledgement upstream |
| `c-groupid`, `s-ip`, `s-hostname`, `s-port` | no counterpart: an acknowledgement names the downstream party by its key, never by an address |
| `s-sid`, `s-ccid` | no counterpart |
| `cs(WAI-Room-Proof)` | where request headers are logged, the proof of a retrieval (§6.2); the record's `proof` is its SHA-256 |
| `s-uri-signing` (RFC 9246 §4.5) | 200 when the request's URI Signing JWT, mapped from a grant (live-rooms §10.3), verified |
The acknowledgement's digest travels in a `remark` directive (RFC 7937 §3.3), `#remark:
wai-settle-ack <digest in hexadecimal>`, before the records of its items. RFC 7937's tools
ignore a remark, and a settlement verifier reads it to find the record a log line is covered by.
### 9.2 What a log cannot carry
Every record of an RFC 7937 file is the serving side's statement. The file's `SHA256-hash`
directive is a hash its writer computes over the file, which detects truncation and carries no
key; `claimed-origin` is the writer's claim of who it is, and `established-origin` the
receiver's note of whom its transport authenticated (RFC 7937 §3.3). No field carries a
signature by the party that received the bytes, and no field binds the bytes served to a
content hash. A log is therefore what a declaration is (§4): the delivering party's own word. A
converter maps a log onto declarations, never onto acknowledgements; an acknowledgement's
counter-signature, the downstream party's signature over the content hashes and byte counts, is
what no log field expresses, and it travels beside the log as the record.
### 9.3 Open caching logging and request routing
SVTA2050 extends RFC 7937's pull mode with batch push and streaming transfer, and defines record
types, one of them minimal for billing; it extends the footprint and capabilities interface to
advertise logging. Its body was not read for this revision, only its published summary, so this
revision maps its records only through the RFC 7937 fields they carry (§9.1); a field it adds is
not mapped, and §9.2 holds for it as for any log a serving side writes. SVTA2007 and SVTA2006
define how an upstream CDN delegates requests to an open cache and provisions that delegation. A
request router that delegates or redirects a request moves no bytes and is not a party of a
settlement. A redirected request reaches a new target, so it carries a proof over the new
target's challenge (live-rooms §10.5.3), and a cache that fills for it records that proof (§6.2).
## 10. Viewers as relays over ciphertext
A viewer that forwards objects to other viewers is a hop for them: it is the upstream party of
the acknowledgements they sign, and it is paid for the bytes they acknowledge, on the terms of
any relay. In an end-to-end encrypted room ([confidentiality](confidentiality.md)) the objects
it forwards are sealed, and their content hash is over the sealed bytes (confidentiality §8.1):
- an acknowledgement names the sealed objects by those content hashes, so it binds the
ciphertext, and the downstream party signs for exactly the bytes that travelled;
- a forwarder needs no key of the room's group: what it is paid for, the bytes, and what it
must prove, the range proof of §5, are both over the sealed bytes, so a forwarder that is not
a member, or a member that forwards a layer it does not open, earns acknowledged bytes without
opening anything;
- the publisher's receipts bind the same sealed bytes (confidentiality §8.1), so a downstream
party checks what it received against them before it acknowledges, and an object a forwarder
altered fails its receipt and is never acknowledged.
The corpus holds a viewer's statement for sealed objects it forwarded, and a range proof over a
sealed object.
## 11. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `ack_invalid` | §3.6, §8.2 step 3 |
| `self_acknowledged` | §3.6, §8.2 step 3 |
| `items_invalid` | §3.6, §8.2 step 3 |
| `items_mismatch` | §3.6, §8.2 step 3 |
| `ack_wrong_party` | §8.2 step 3 |
| `ack_outside_period` | §8.2 step 3 |
| `ack_overlap` | §3.7, §8.2 step 4 |
| `ack_chain_broken` | §3.7, §8.2 step 4 |
| `declared_and_acknowledged` | §4.2, §8.2 step 4 |
| `range_invalid` | §5.3 |
| `range_outside_object` | §5.3 |
| `range_proof_mismatch` | §5.4 |
| `range_not_acknowledged` | §5.4, §5.6 |
| `statement_invalid` | §7.5, §8.2 step 1 |
| `paid_without_grant` | §6.1, §7.6 |
| `bytes_not_acknowledged` | §7.6 |
| `statement_mismatch` | §7.6 |
| `hop_not_party` | §7.7, §8.2 step 5 |
| `span_outside_period` | §7.7, §8.2 step 5 |
A verifier of this extension also raises `truncated`, `malformed` and `unknown_version` for a
record, a range proof or a statement that does not read; `bad_key` and `bad_signature` for their
signatures; `record_missing` (live-metering §9) for an input not given; `classes_summed`
(live-metering §9) for a total over classes; live-rooms §11's `outside_grant_window`,
`grant_wrong_principal`, `seek_not_granted`, `pop_malformed`, `pop_wrong_grant`,
`pop_wrong_key`, `pop_bad_signature` and `pop_replayed` for a grant and its proof; every code a
descriptor and its grants raise (live-rooms §3.4, §4.3), at `room`; and every code
live-metering §3.6 raises for a per-hop record, at `hop`.
## 12. Conformance
[`live-conformance/settlement/`](../live-conformance/settlement/) holds the corpus, in one file,
`vectors.json`, written from fixed seeds by `wai_settlement_vectors gen`. It holds objects,
rooms, acknowledgements and declarations with their item lists, proof records, per-hop records,
and:
- **statements**, each with the outcome §8.3 gives it. **Accepted**: a relay paid by a cache
downstream of it and by a viewer, with a declaration for a viewer that did not sign and one for
viewers with no key, and its energy in two classes, each in its own total; a cache paid for a
viewer's fetch below the live edge under its grant and proof, a range of an object that viewer
received in part, and a fetch served free; an origin paid for a cache's fills billed to the
grant whose request they filled for, beside a fill no grant was presented for; a viewer paid
for sealed objects it forwarded, under no tariff; and a relay with an unmetered hop record,
whose energy is partial. **Refused**: each reason of §11 at least once, each vector a control
of one defect, among them a statement billing bytes no acknowledgement covers, an
acknowledgement signed by the hop itself, a paid fetch with no grant, a total summing joules
across classes, a statement that does not recompute, and an acknowledgement replayed from
another period; and codes of the extensions this one reads.
- **ranges**: range proofs, each checked against a content hash, some as the answer to an audit
of an acknowledged item, with the outcome of §8.3. **Accepted**: a range inside an object
across chunk boundaries, its head, its tail (which establishes the length), a range within one
chunk, a whole object of one chunk and of several, a range of a sealed object, an audit
answered, and an interior range whose stated length is wrong and leaves the tree's shape
unchanged, accepted with its length not established (§5.5). **Refused**: a range past the
object's end, an empty range, bytes of another object at the same offsets, a range past the
true end under a stated length that is longer, a chaining value dropped, added or changed, a
proof for another content hash, an audit answered with a range the item does not cover, and
records that do not read.
A conforming verifier gives every statement and every range proof the outcome, the reason, the
place and the figures the corpus states. `wai_settlement_vectors verify` and the test
`tests/settlement.rs` check that the file regenerates byte for byte, that `wai-rs` gives every
vector its outcome, that every reason of §11 refuses a vector, and that §11 lists exactly the
reference's codes. `live-conformance/settlement_verify.py` is the second implementation, written
from this text: it shares no code with `wai-rs`, verifies signatures with WAI's strict rule
written from RFC 8032 (`claim-conformance/_ed25519.py`), computes BLAKE3 and its chaining values
with `claim-conformance/_blake3.py` (the module the HTTP head proof's verifier reads its tree
from), reads rooms and grants with `live_rooms_verify.py` and per-hop records with
`live_metering_verify.py`, recomputes every figure itself, gives every vector the same outcome,
requires every mutated copy of the corpus to fail, and requires the corpus to fail a copy of
itself with each of its checks dropped.
## 13. Security and privacy considerations
- **Who pays on whose word.** A hop's bill rests on its downstream parties' signatures; a
declaration rests on its own, and is stated apart. A hop that inflates what it delivered can
inflate only its declarations, which bill nothing.
- **Collusion and sybils.** A hop and a downstream party that collude can acknowledge bytes that
never moved, and no record can tell; a range proof shows that a party holds bytes, not that
they crossed a link. A payer therefore honours acknowledgements only from keys it can place: a
hop's key that the hop's origin lists with the authority of its role (jwp-receipts §7.5,
`live/relay`, `live/compositor`, `live/ingest`), and a sink whose entry names a grant, whose
`principal` the acknowledgement must be (§6.3 step 3), which only the room authority can issue.
A statement names every downstream key in its lines, so a payer sees whom it is paying for.
- **Replay.** Windows are signed and chained (§3.7); a record counts only in the period that
holds its window, and a proof pays for one record. A statement for one period cannot reuse a
record of another, and two statements of one party for periods that do not overlap cannot
name one record.
- **Paid retrieval.** A retrieval is billed only to a grant whose holder signed a proof, so a
party that holds no grant cannot run up a grant holder's bill, and a cache cannot bill a fill
to a grant without a proof its holder made. The proof binds a session or a request, not an
object, so a cache holding a proof can name it for any fill of that session's requests: the
rule that one proof pays for one record bounds this to one window.
- **Range proofs.** A proof binds bytes to offsets of a content hash. It establishes an object's
length only from its last chunk (§5.5), and a verifier reports whether it did.
- **What an acknowledgement reveals, and to whom.** An acknowledgement and its settlement are the
mechanics of the delivery service itself, as its receipts are: a sink that takes delivery from a
service signs them as part of that delivery, and this extension sets no gate of its own before
it does. What each party learns:
- the **upstream party** learns, from an acknowledgement signed for it, the downstream party's
key, its window, and the content hashes, byte ranges and counts it received: what that key
received from it, which it delivered and so already knew, now counter-signed;
- a **party verifying a statement** (the payer, or a party settling between them) learns the
same of every record the statement names, the grants and proofs of its retrievals, and the
party's per-hop records; a sink's key and grant name the principal the room authority
granted (live-rooms §13);
- a **party shown only a statement** learns its lines (each downstream key or grant digest, its
object class, objects, bytes and amount), its declared totals and its energy, and no object:
the statement names records by digest and never names an object;
- **nobody else**: the records and item lists travel between the parties that settle, and
nothing here publishes them.
A key per room keeps a viewer's acknowledgements in one room from linking to its others
(live-events §11); a viewer admitted with no key is declared, not acknowledged, and is named by
no key at all. Where a regime asks the operator to state the lawful basis on which it processes
what acknowledgements reveal, the operator states it as a `lawful-basis` declaration
(jwp-receipts §2.4.6), one purpose for settlement with its own basis; this extension does not
restate or check it.
- **What a statement is not.** It is not an invoice and not a payment: it is the recomputable
figures a payment would rest on. Its amount is an integer of millionths of a minor unit,
rounded down once per line.

View file

@ -21,6 +21,7 @@ and the independent verifier of each, beside it.
| [`synthesis/`](synthesis/) | [synthesis](../extensions/synthesis.md) (`wai.media.synthesis`) | `synthesis_verify.py` |
| [`usage/`](usage/) | [ai-usage](../extensions/ai-usage.md) (`wai.ai.usage`) | `ai_usage_verify.py` |
| [`contribution/`](contribution/) | [contribution](../extensions/contribution.md) (`wai.media.contribution`) | `contribution_verify.py` |
| [`settlement/`](settlement/) | [settlement](../extensions/settlement.md) (`wai.delivery.settlement`) | `settlement_verify.py` |
**Provenance.** Generated here: `wai_live_events_vectors gen` writes `events/`,
`wai_live_room_vectors gen` writes `rooms/`, `wai_capture_take_vectors gen` writes
@ -30,15 +31,16 @@ writes `recordings/`, `wai_timed_text_vectors gen` writes `captions/` and
`steering/`, `wai_live_moderation_vectors gen` writes `moderation/`,
`wai_live_watermark_vectors gen` writes `watermark/`, `wai_live_ads_vectors gen` writes
`ads/`, `wai_live_provenance_vectors gen` writes `c2pa-live/`, `wai_synthesis_vectors gen`
writes `synthesis/`, `wai_ai_usage_vectors gen` writes `usage/` and
`wai_contribution_vectors gen` writes `contribution/`, each from records authored in code, from
fixed seeds (`events/README.md`, `rooms/README.md`, `takes/README.md`,
`compose/README.md`, `recordings/README.md`, `captions/README.md`, `metering/README.md`,
`steering/README.md`, `moderation/README.md`, `watermark/README.md`, `ads/README.md`,
`c2pa-live/README.md`, `synthesis/README.md`, `usage/README.md`, `contribution/README.md`);
`c2pa-live/` cuts its media from the packaging corpus's AV1 floor, and `contribution/` generates
its pictures with `wai.generate.spectral` and the model file of that capability's conformance
vector. `joule_moq_e2ee_vectors gen` (in
writes `synthesis/`, `wai_ai_usage_vectors gen` writes `usage/`,
`wai_contribution_vectors gen` writes `contribution/` and `wai_settlement_vectors gen` writes
`settlement/`, each from records authored in code, from fixed seeds (`events/README.md`,
`rooms/README.md`, `takes/README.md`, `compose/README.md`, `recordings/README.md`,
`captions/README.md`, `metering/README.md`, `steering/README.md`, `moderation/README.md`,
`watermark/README.md`, `ads/README.md`, `c2pa-live/README.md`, `synthesis/README.md`,
`usage/README.md`, `contribution/README.md`, `settlement/README.md`); `c2pa-live/` cuts its media
from the packaging corpus's AV1 floor, and `contribution/` generates its pictures with
`wai.generate.spectral` and the model file of that capability's conformance vector.
`joule_moq_e2ee_vectors gen` (in
`joule-code/joule-code-rs/crates/joule-moq-mls`) writes `e2ee/vectors.json` from fixed seeds,
and `e2ee/` also holds the test vectors RFC 9605 and `draft-ietf-moq-secure-objects-01`
publish, as printed (`e2ee/README.md`). One recorded record: `metering/relay-record.wmh` and
@ -65,4 +67,5 @@ python3 c2pa_live_verify.py c2pa-live
python3 synthesis_verify.py synthesis
python3 ai_usage_verify.py usage
python3 contribution_verify.py contribution
python3 settlement_verify.py settlement
```

View file

@ -0,0 +1,86 @@
# live-conformance/settlement — delivery settlement
The corpus of [`extensions/settlement.md`](../../extensions/settlement.md) §12: objects, a room
with its grants, acknowledgements and declarations (`WDA1`) with their item lists, the proof
records (`WRP1`, [live-rooms](../../extensions/live-rooms.md) §10.5.1) their retrievals carried,
per-hop records (`WMH1`, [live-metering](../../extensions/live-metering.md) §3), and two kinds of
vector: settlement statements (`WSS1`) and range proofs (`WBR1`), each with the outcome a
verifier gives it.
It sits beside [`rooms/`](../rooms/) and [`metering/`](../metering/) because its grants and proofs
are live-rooms', read by that corpus's second implementation, `live_rooms_verify.py`, and its hop
records are live-metering's, read by `live_metering_verify.py`.
**Settlement equivalence.** For each statement, a conforming verifier given the statement, the
pool's records, item lists, proofs and hop records less those the vector `withhold`s (by digest,
§8.2) and with those it adds in `extra`, and the rooms it names, reaches the outcome the vector
states (§8.3):
- `refuse`: the `reason` and where, `at`: `["statement"]`, `["room", id]` or `["room", id,
"grant", i]`, `["record", k]` for the record `k` the statement names, `["record", k, "entry",
j]` for one of its entries, or `["hop", k]`;
- `accept`: the statement's `digest`, its `currency` (or `null`), the acknowledged `objects` and
`bytes` by object class, each line (class, payer, objects, bytes, amount, and whether it is
`free`), the `unacknowledged` figures, the energy's `coverage`, `unmetered` and `totals` by
class label, and the `amount`.
For each range proof, a conforming verifier given the proof, the `content_hash` it is checked
against and the item of the `audit` it answers (or none, when `null`) reaches the outcome it
states: `refuse` with its `reason`, or `accept` with the range, `envelope_len`, the `covered`
chunks as offsets into `X`, and whether the length was established (§5.5). A verifier that
reaches another outcome, reason, place or figure does not conform.
**Provenance.** Generated here: `wai_settlement_vectors gen` writes `vectors.json` from figures
written in code, from fixed seeds: each key is SHA-256 of the corpus's fixed label and the
principal's index, each object's bytes a SHA-256 stream of another fixed label and the object's
name, each proof's challenge SHA-256 of a third label and an index, and every time is a constant
(the room's `t0` is 2026-10-16T00:00:00Z, and every statement settles its first minute); Ed25519
signing is deterministic (RFC 8032), so the file is the same bytes on every machine. The objects
are opaque test bytes standing for envelopes, which nothing parses or decodes; those named `s*`
stand for sealed objects, whose bytes are opaque as ciphertext is. The byte counts, prices and
joule figures are synthetic constants chosen to exercise the rules, and were measured by
nothing: the corpus states what the rules compute and claims nothing about what any delivery
costs. The origins and URIs are `.example` names (RFC 2606). The generator asserts that each
vector reaches the outcome it was written to have before it writes it.
## Layout
One file, `vectors.json`:
- `principals`: the keys, by name (informative): a room authority and clock, an origin, a relay,
an edge cache, four viewers, a viewer that forwards, and a stranger.
- `objects`: each object's content hash and length (informative).
- `rooms`: `lesson`, its `descriptor` (`WRD1`) and its `grants` (`WRG1`) in order: viewer A's
with the seek power, viewer B's, C's and D's without it, and viewer A's second grant, which
expires 15 s into the room; and `lesson_forged`, the same with a grant the stranger signed.
`grants` names their digests.
- `records`: every acknowledgement and declaration a vector names, each with its `items`, one
item list per entry, and its digest (informative; a verifier finds a record by the digest of
its bytes).
- `proofs`: proof records by name, among them one by another key, one whose signature is not the
key's, one naming another grant and one that does not read.
- `hops`: per-hop records of the relay (a counter reading, a model, an unmetered marker, one past
the period and one whose class breaks live-metering §3.6) and one of the edge.
- `statements`: accepted, a relay paid by the edge and a viewer with two declarations and its
energy in two classes; the edge paid for a granted fetch, a range of an object received in
part, and a fetch served free; the origin paid for fills billed to the grant whose request
they filled for, beside a fill no grant was presented for; a viewer paid for sealed objects it
forwarded, under no tariff; and a relay with an unmetered hop record. Refused, each reason of
§11 and the codes it reuses, each a control of one defect.
- `ranges`: accepted, a range across chunk boundaries, the head, the tail, a range within one
chunk, whole objects of one and of several chunks, a range of a sealed object, an audit
answered, and an interior range whose stated length is wrong and leaves the tree's shape
unchanged; refused, a range past the end, an empty range, another object's bytes, a range past
the true end under a longer length, chaining values dropped, added and changed, a proof checked
against another content hash and one naming another, an audit the item does not cover, and
records that do not read.
## Regenerate and verify
```
cd wai/wai-rs
cargo run --no-default-features --features settlement --bin wai_settlement_vectors -- gen ../live-conformance/settlement
cargo run --no-default-features --features settlement --bin wai_settlement_vectors -- verify ../live-conformance/settlement
cargo test --no-default-features --features settlement --test settlement
python3 -I ../live-conformance/settlement_verify.py ../live-conformance/settlement
```

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,787 @@
#!/usr/bin/env python3
"""Independent verifier of the delivery settlement corpus (extensions/settlement.md).
Written from the extension's text. It shares no code with wai-rs, which generates the corpus,
and never runs it. It checks Ed25519 signatures with ../claim-conformance/_ed25519.py, WAI's
strict rule (SPEC §7 *Signatures*) written from RFC 8032 with the standard library, and
computes BLAKE3, its subtree chaining values and its parent nodes with
../claim-conformance/_blake3.py, written from the BLAKE3 specification: the module the HTTP head
proof's verifier reads its tree from. A room's descriptor, its grants and a grant's proof record
are read by the live-rooms corpus's own second implementation, live_rooms_verify.py (live-rooms
§3, §4, §10.5.1), and a per-hop record by the live-metering corpus's, live_metering_verify.py
(live-metering §3.6), each written from its text alone. Everything else is here:
- the acknowledgement and the declaration (§3, §4): reading, rules, signature, item lists, the
tree root of jwp-receipts §2.2, chains;
- the range proof (§5): reading, the covered chunks and the root over BLAKE3's chunk tree;
- a grant and its proof on an entry (§6.3);
- the statement (§7): reading, rules, signature, and every figure recomputed;
- verification in §8.2's order, with each refusal's place.
For every statement and range proof it requires the outcome the corpus states; of the corpus,
that every reason of §11 appears in it and that no other file is there. Then it mutates copies of
the corpus, one defect each, and requires every copy to fail; and it drops each of its own checks
in turn and requires the corpus to catch the weakened verifier.
python3 settlement_verify.py <live-conformance/settlement>
Exit status 0 only if every case verifies, every mutated copy fails and every weakened verifier
fails. Python 3.8 or later; standard library only.
"""
import copy
import hashlib
import json
import os
import struct
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
import live_events_verify as LEV # noqa: E402
import live_rooms_verify as LRV # noqa: E402
import live_metering_verify as LMV # noqa: E402
import _ed25519 as ED # noqa: E402
import _blake3 as B3 # noqa: E402
EXTENSION = os.path.join(HERE, "..", "extensions", "settlement.md")
Refuse = LEV.Refuse
ACK_MAGIC, RANGE_MAGIC, STATEMENT_MAGIC = b"WDA1", b"WBR1", b"WSS1" # §3.1, §5.2, §7.1
ACK_DOMAIN = b"wai:settle-ack\x01" # §3.5
ITEMS_DOMAIN = b"wai:settle-items\x01" # §3.2
STATEMENT_DOMAIN = b"wai:settle-statement\x01" # §7.4
OBJECT_DOMAIN = b"moq-jwp:object\x01" # jwp-receipts §2.1
LEAF, NODE = b"moq-jwp:merkle-leaf\x01", b"moq-jwp:merkle-node\x01" # jwp-receipts §2.2
PROOF_DOMAIN = b"wai:live-room-proof\x01" # live-rooms §10.5.1
SEEK = 16 # live-rooms §3.3, bit 4
ACK, DECLARATION = 1, 2
HOP, SINK = 1, 2
LIVE, FETCH, FILL, ANCILLARY = 1, 2, 3, 4
CLASS_NAMES = {LIVE: "live", FETCH: "fetch", FILL: "fill", ANCILLARY: "ancillary"}
RETRIEVAL = (FETCH, FILL)
LABELS = {2: "ModelBased", 3: "Estimator", 4: "OnChipCounter", 5: "CalibratedInstrument"}
ZERO = bytes(32)
U64 = 2 ** 64 - 1
CHUNK = 1024
DLEN = 15 # the length of jwp-receipts §2.1's domain
PER = 10 ** 9 # §7.3: prices per 10^9 bytes
SKIP = set()
# Signature checks are pure functions of their inputs, and the corpus checks the same records under
# every mutated copy and every weakened verifier: each is computed once.
_ED_VERIFY = ED.verify
_ED_MEMO = {}
def _ed_verify(pk, msg, sig, skip=()):
k = (bytes(pk), bytes(msg), bytes(sig), tuple(skip))
if k not in _ED_MEMO:
_ED_MEMO[k] = _ED_VERIFY(pk, msg, sig, skip=skip)
return _ED_MEMO[k]
ED.verify = _ed_verify
def reasons_of_extension():
"""§11's codes, read from the text."""
with open(EXTENSION, encoding="utf-8") as f:
text = f.read()
at = text.index("\n## 11. Reasons")
end = text.index("\n## ", at + 1)
return [line.split("`")[1] for line in text[at:end].splitlines() if line.startswith("| `")]
REASONS = reasons_of_extension()
REUSED = ["truncated", "malformed", "unknown_version", "bad_key", "bad_signature", "record_missing", "classes_summed",
"outside_grant_window", "grant_wrong_principal", "seek_not_granted", "pop_malformed", "pop_wrong_grant",
"pop_wrong_key", "pop_bad_signature", "pop_replayed"]
class Stop(Exception):
"""A refusal and its place (§8.2)."""
def __init__(self, reason, at):
super().__init__(reason)
self.reason, self.at = reason, at
def sha(*parts):
h = hashlib.sha256()
for p in parts:
h.update(p)
return h.digest()
def given_digest(b):
"""§8.2: SHA-256 over all but the last 64 bytes."""
return sha(b[:max(len(b) - 64, 0)])
class R:
"""A cursor; running out is `truncated`."""
def __init__(self, b, p=0):
self.b, self.p = b, p
def take(self, n):
if self.p + n > len(self.b):
raise Refuse("truncated")
s = self.b[self.p:self.p + n]
self.p += n
return s
def u8(self):
return self.take(1)[0]
def u16(self):
return struct.unpack("<H", self.take(2))[0]
def u32(self):
return struct.unpack("<I", self.take(4))[0]
def u64(self):
return struct.unpack("<Q", self.take(8))[0]
def read_head(b, magic):
"""§3.6: the magic, then version 1."""
if len(b) < 4:
raise Refuse("truncated")
if b[:4] != magic:
raise Refuse("malformed")
if len(b) < 5:
raise Refuse("truncated")
if b[4] != 1:
raise Refuse("unknown_version")
def check_sig(key, domain, body, sig):
bad = ED.verify(key, domain + body, sig)
if bad:
raise Refuse(bad)
# ---------------------------------------------------------------- §2, §3.4: the tree and items
def tree_root(hashes):
"""§2: jwp-receipts §2.2's root in its first form."""
if not hashes:
return ZERO
level = [B3.blake3(LEAF + h) for h in hashes]
while len(level) > 1:
if len(level) % 2:
level.append(level[-1])
level = [B3.blake3(NODE + level[k] + level[k + 1]) for k in range(0, len(level), 2)]
return level[0]
def read_items(b):
"""§3.6: an item list, or Refuse(items_invalid)."""
if len(b) % 48:
raise Refuse("items_invalid")
items = [(b[k:k + 32], int.from_bytes(b[k + 32:k + 40], "little"), int.from_bytes(b[k + 40:k + 48], "little"))
for k in range(0, len(b), 48)]
if "items-rules" not in SKIP:
if any(s >= e for _, s, e in items):
raise Refuse("items_invalid")
for x in range(len(items)):
for y in range(x + 1, len(items)):
if items[x][0] == items[y][0] and overlaps(items[x][1:], items[y][1:]):
raise Refuse("items_invalid")
return items
def overlaps(a, b):
return a[0] < b[1] and b[0] < a[1]
def recomputes(e, items, raw):
"""§3.6: the item list against its entry."""
if "items-recompute" in SKIP:
return True
return (len(items) == e["objects"] and sum(z - s for _, s, z in items) == e["bytes"]
and tree_root([h for h, _, _ in items]) == e["root"] and sha(ITEMS_DOMAIN, raw) == e["items"])
# ---------------------------------------------------------------- §3 the acknowledgement
def read_ack(b):
"""§3.6: a record, its rules, then its signature."""
read_head(b, ACK_MAGIC)
r = R(b, 5)
a = {"mode": r.u8(), "role": r.u8(), "upstream": r.take(32), "downstream": r.take(32), "start": r.u64(),
"end": r.u64(), "prev": r.take(32), "entries": []}
for _ in range(r.u16()):
a["entries"].append({"class": r.u8(), "grant": r.take(32), "proof": r.take(32), "objects": r.u32(),
"bytes": r.u64(), "root": r.take(32), "items": r.take(32)})
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
def entry_ok(e):
if e["class"] not in CLASS_NAMES or e["objects"] < 1 or e["bytes"] < e["objects"]:
return False
if e["class"] in RETRIEVAL:
return (e["grant"] == ZERO) == (e["proof"] == ZERO)
return e["proof"] == ZERO
keys = [(e["class"], e["grant"], e["proof"]) for e in a["entries"]]
rules = (a["mode"] in (ACK, DECLARATION) and a["role"] in (HOP, SINK) and a["start"] < a["end"]
and a["upstream"] != ZERO and (a["mode"] == DECLARATION or a["downstream"] != ZERO)
and a["entries"] and all(x < y for x, y in zip(keys, keys[1:])) and all(entry_ok(e) for e in a["entries"]))
if "ack-rules" not in SKIP and not rules:
raise Refuse("ack_invalid")
if "self-ack" not in SKIP and a["mode"] == ACK and a["downstream"] == a["upstream"]:
raise Refuse("self_acknowledged")
if "ack-signature" not in SKIP:
check_sig(a["upstream"] if a["mode"] == DECLARATION else a["downstream"], ACK_DOMAIN, b[:-64], sig)
a["digest"] = sha(b[:-64])
return a
# ---------------------------------------------------------------- §5 range proofs
def left(n):
"""§5.1: 1024 · 2^⌊log₂((n − 1)/1024)⌋, for n above 1024."""
q = (n - 1) // CHUNK
return CHUNK << (q.bit_length() - 1)
def covered(n, start, end):
"""§5.2: the covered chunks, as offsets into X."""
c0 = (DLEN + start) // CHUNK * CHUNK
c1 = min(DLEN + n, -(-(DLEN + end) // CHUNK) * CHUNK)
return c0, c1
def read_range(b):
"""§5.3."""
read_head(b, RANGE_MAGIC)
if len(b) < 62:
raise Refuse("truncated")
r = R(b, 5)
p = {"content_hash": r.take(32), "envelope_len": r.u64(), "start": r.u64(), "end": r.u64()}
n = r.u8()
if "range-bounds" not in SKIP:
if p["start"] >= p["end"]:
raise Refuse("range_invalid")
if p["end"] > p["envelope_len"] or DLEN + p["envelope_len"] > U64:
raise Refuse("range_outside_object")
c0, c1 = covered(p["envelope_len"], p["start"], p["end"])
want = 62 + 32 * n + (c1 - max(c0, DLEN))
if len(b) < want:
raise Refuse("truncated")
if len(b) > want:
raise Refuse("malformed")
p["cvs"] = [r.take(32) for _ in range(n)]
p["bytes"] = b[r.p:]
return p
def verify_range(p, h, audit):
"""§5.4, in order; the facts of §8.3."""
if "range-hash" not in SKIP and p["content_hash"] != h:
raise Refuse("range_proof_mismatch")
if audit is not None and "range-audit" not in SKIP:
if audit[0] != h or p["start"] < audit[1] or p["end"] > audit[2]:
raise Refuse("range_not_acknowledged")
L = DLEN + p["envelope_len"]
c0, c1 = covered(p["envelope_len"], p["start"], p["end"])
xc = (OBJECT_DOMAIN if c0 == 0 else b"") + p["bytes"]
if c0 == 0 and c1 == L:
if p["cvs"]:
raise Refuse("range_proof_mismatch")
root = B3.blake3(xc)
else:
cvs = list(p["cvs"])
def cv(s, n):
if s + n <= c0 or s >= c1:
if not cvs:
raise Refuse("range_proof_mismatch")
return cvs.pop(0)
if c0 <= s and s + n <= c1:
return B3.subtree_cv(xc[s - c0:s - c0 + n], s)
if n <= CHUNK:
raise Refuse("range_proof_mismatch")
k = left(n)
return B3.parent_cv(cv(s, k), cv(s + k, n - k))
k = left(L)
a = cv(0, k)
z = cv(k, L - k)
if cvs and "range-exact" not in SKIP:
raise Refuse("range_proof_mismatch")
root = B3.parent_cv(a, z, root=True)
if root != h and "range-root" not in SKIP:
raise Refuse("range_proof_mismatch")
return {"outcome": "accept", "start": p["start"], "end": p["end"], "envelope_len": p["envelope_len"],
"covered": [c0, c1], "length_established": c1 == L}
def range_outcome(v):
audit = None
if v["audit"] is not None:
audit = (bytes.fromhex(v["audit"]["content_hash"]), v["audit"]["start"], v["audit"]["end"])
try:
return verify_range(read_range(bytes.fromhex(v["proof"])), bytes.fromhex(v["content_hash"]), audit)
except Refuse as e:
return {"outcome": "refuse", "reason": e.reason}
# ---------------------------------------------------------------- §7 the statement
def read_statement(b):
"""§7.5: a statement, its rules, then its signature."""
read_head(b, STATEMENT_MAGIC)
r = R(b, 5)
s = {"party": r.take(32), "start": r.u64(), "end": r.u64()}
s["records"] = [r.take(32) for _ in range(r.u16())]
s["hops"] = [r.take(32) for _ in range(r.u16())]
s["currency"] = r.take(3)
s["prices"] = [r.u64() for _ in range(4)]
s["classes"] = [(r.u64(), r.u64()) for _ in range(4)]
s["unack"] = (r.u32(), r.u64(), r.u64())
s["unmetered"] = r.u16()
s["totals"] = [(r.u8(), r.u32(), r.u64()) for _ in range(r.u8())]
s["lines"] = []
for _ in range(r.u16()):
s["lines"].append({"kind": r.u8(), "payee": r.take(32), "class": r.u8(), "payer": r.take(32),
"objects": r.u64(), "bytes": r.u64(), "amount": r.u64()})
s["amount"] = r.u64()
sig = r.take(64)
if r.p != len(b):
raise Refuse("malformed")
if any(t[0] == 0 for t in s["totals"]):
raise Refuse("classes_summed")
def ascending(xs):
return all(x < y for x, y in zip(xs, xs[1:]))
tariff = all(p == 0 for p in s["prices"]) if s["currency"] == bytes(3) else all(0x41 <= c <= 0x5A for c in s["currency"])
keys = [(x["kind"], x["payee"], x["class"], x["payer"]) for x in s["lines"]]
rules = (s["start"] < s["end"] and s["records"] and ascending(s["records"]) and ascending(s["hops"]) and tariff
and all(2 <= t[0] <= 5 and t[1] > 0 for t in s["totals"]) and ascending([t[0] for t in s["totals"]])
and all(x["kind"] == 1 and x["payee"] == s["party"] and x["class"] in CLASS_NAMES and x["objects"] > 0
for x in s["lines"])
and ascending(keys))
if "statement-rules" not in SKIP and not rules:
raise Refuse("statement_invalid")
if "statement-signature" not in SKIP:
check_sig(s["party"], STATEMENT_DOMAIN, b[:-64], sig)
s["digest"] = sha(b[:-64])
return s
def amount_of(prices, c, nbytes):
"""§7.3: ⌊bytes × p / 10^9⌋."""
return nbytes * prices[c - 1] // PER
def figures(s, acks, hops):
"""§7.6, §7.7: every figure, recomputed."""
lines = {}
unack = [0, 0, 0]
for a in acks:
if a["mode"] == DECLARATION:
unack[0] += 1
for e in a["entries"]:
unack[1] += e["objects"]
unack[2] += e["bytes"]
continue
for e in a["entries"]:
payer = e["grant"] if e["class"] in RETRIEVAL else a["downstream"]
x = lines.setdefault((e["class"], payer), [0, 0, 0])
x[0] += e["objects"]
x[1] += e["bytes"]
classes = [[0, 0] for _ in range(4)]
total = 0
for (c, payer), x in lines.items():
x[2] = 0 if (c in RETRIEVAL and payer == ZERO) else amount_of(s["prices"], c, x[1])
classes[c - 1][0] += x[0]
classes[c - 1][1] += x[1]
total += x[2]
totals, unmetered = {}, 0
for h in hops:
if h["class"] is None:
unmetered += 1
else:
t = totals.setdefault(h["class"][0], [0, 0])
t[0] += 1
t[1] += h["joules"]
return {"lines": lines, "classes": classes, "unack": unack, "unmetered": unmetered, "totals": totals, "amount": total}
def check_figures(s, f):
"""§7.6's comparison, in its order."""
if "free-line" not in SKIP:
for x in s["lines"]:
if x["class"] in RETRIEVAL and x["payer"] == ZERO and x["amount"] != 0:
raise Refuse("paid_without_grant")
if "bytes-covered" not in SKIP:
for x in s["lines"]:
got = f["lines"].get((x["class"], x["payer"]))
if got is None or x["objects"] > got[0] or x["bytes"] > got[1]:
raise Refuse("bytes_not_acknowledged")
if "classes-summed" not in SKIP:
tags = sorted(f["totals"])
for tag, _, joules in s["totals"]:
if tag in f["totals"] and joules != f["totals"][tag][1]:
others = [x for x in tags if x != tag]
for mask in range(1, 1 << len(others)):
extra = sum(f["totals"][others[i]][1] for i in range(len(others)) if mask >> i & 1)
if extra + f["totals"][tag][1] == joules:
raise Refuse("classes_summed")
if "figures" in SKIP:
return
same = (len(s["lines"]) == len(f["lines"])
and all(f["lines"].get((x["class"], x["payer"])) == [x["objects"], x["bytes"], x["amount"]] for x in s["lines"])
and [list(c) for c in s["classes"]] == f["classes"] and list(s["unack"]) == f["unack"]
and s["unmetered"] == f["unmetered"]
and len(s["totals"]) == len(f["totals"]) and all(f["totals"].get(t) == [n, j] for t, n, j in s["totals"])
and s["amount"] == f["amount"])
if not same:
raise Refuse("statement_mismatch")
# ---------------------------------------------------------------- §6.3 grants and proofs
def grant_check(a, e, rooms, proofs):
held = [g for r in rooms for g in r["grants"] if g["digest"] == e["grant"]]
if not held:
raise Refuse("record_missing")
g = held[0]
if "grant-window" not in SKIP and not (g["nbf"] * 10 ** 6 <= a["start"] and a["end"] <= g["exp"] * 10 ** 6):
raise Refuse("outside_grant_window")
if e["class"] not in RETRIEVAL:
if "grant-principal" not in SKIP and g["principal"] != a["downstream"]:
raise Refuse("grant_wrong_principal")
return
b = proofs.get(e["proof"])
if b is None:
raise Refuse("record_missing")
if "proof-checks" not in SKIP:
p = LRV.read_proof(b)
if p["grant"] != g["digest"]:
raise Refuse("pop_wrong_grant")
if p["key"] != g["principal"]:
raise Refuse("pop_wrong_key")
if ED.verify(p["key"], PROOF_DOMAIN + p["body"], p["sig"]) is not None:
raise Refuse("pop_bad_signature")
if "seek" not in SKIP and e["class"] == FETCH and not g["powers"] & SEEK:
raise Refuse("seek_not_granted")
# ---------------------------------------------------------------- §8 verification
def inputs_of(doc, v):
"""The pool, less what the vector withholds, with what it adds; its rooms."""
withhold = {bytes.fromhex(d) for d in v["withhold"]}
records, items, proofs, hops = {}, {}, {}, {}
for r in doc["records"] + v["extra"]["records"]:
b = bytes.fromhex(r["record"])
d = given_digest(b)
if d in withhold and "name" in r:
continue
records[d] = b
items[d] = [bytes.fromhex(x) for x in r["items"]]
for raw in [p["proof"] for p in doc["proofs"]] + v["extra"]["proofs"]:
b = bytes.fromhex(raw)
if sha(b) not in withhold:
proofs[sha(b)] = b
for h in doc["hops"]:
b = bytes.fromhex(h["hop"])
if given_digest(b) not in withhold:
hops[given_digest(b)] = b
rooms = [doc["rooms"][n] for n in v["rooms"]]
return records, items, proofs, hops, rooms
def verify_statement(doc, v):
records, items, proofs, hops, room_inputs = inputs_of(doc, v)
# Step 1.
try:
s = read_statement(bytes.fromhex(v["statement"]))
except Refuse as e:
raise Stop(e.reason, ["statement"])
# Step 2.
rooms = []
for ri in room_inputs:
desc = bytes.fromhex(ri["descriptor"])
try:
d = LRV.read_descriptor(desc)
except Refuse as e:
raise Stop(e.reason, ["room", given_digest(desc).hex()])
held, err = LRV.check_grants(d, [bytes.fromhex(g) for g in ri["grants"]])
if err is not None and "room-grants" not in SKIP:
raise Stop(err[1], ["room", d["room"].hex(), "grant", err[0]])
rooms.append({"d": d, "grants": held or []})
# Step 3.
acks = []
for k, d in enumerate(s["records"]):
if d not in records:
raise Stop("record_missing", ["record", k])
try:
a = read_ack(records[d])
except Refuse as e:
raise Stop(e.reason, ["record", k])
if "upstream" not in SKIP and a["upstream"] != s["party"]:
raise Stop("ack_wrong_party", ["record", k])
if "period" not in SKIP and not (s["start"] <= a["start"] and a["end"] <= s["end"]):
raise Stop("ack_outside_period", ["record", k])
a["items_read"] = []
for j, e in enumerate(a["entries"]):
lists = items.get(d, [])
if j >= len(lists):
raise Stop("record_missing", ["record", k, "entry", j])
try:
its = read_items(lists[j])
except Refuse as x:
raise Stop(x.reason, ["record", k, "entry", j])
if not recomputes(e, its, lists[j]):
raise Stop("items_mismatch", ["record", k, "entry", j])
a["items_read"].append(its)
if e["grant"] != ZERO:
try:
grant_check(a, e, rooms, proofs)
except Refuse as x:
raise Stop(x.reason, ["record", k, "entry", j])
acks.append(a)
# Step 4.
chains = {}
for k, a in enumerate(acks):
chains.setdefault((a["mode"], a["upstream"], a["downstream"]), []).append(k)
before = {}
for ks in chains.values():
ks.sort(key=lambda k: (acks[k]["start"], k))
for p, q in zip(ks, ks[1:]):
before[q] = p
for k, a in enumerate(acks):
if k in before:
p = acks[before[k]]
if "chain-overlap" not in SKIP and p["end"] > a["start"]:
raise Stop("ack_overlap", ["record", k])
if "chain-prev" not in SKIP and a["prev"] != p["digest"]:
raise Stop("ack_chain_broken", ["record", k])
if "declared" not in SKIP:
for k, a in enumerate(acks):
if a["mode"] != DECLARATION or a["downstream"] == ZERO:
continue
mine = [x for its in a["items_read"] for x in its]
for b in acks:
if b["mode"] != ACK or b["downstream"] != a["downstream"] or not overlaps((a["start"], a["end"]), (b["start"], b["end"])):
continue
theirs = [x for its in b["items_read"] for x in its]
if any(x[0] == y[0] and overlaps(x[1:], y[1:]) for x in mine for y in theirs):
raise Stop("declared_and_acknowledged", ["record", k])
if "pop-replayed" not in SKIP:
named = {}
for k, a in enumerate(acks):
for j, e in enumerate(a["entries"]):
if e["proof"] == ZERO:
continue
if e["proof"] in named and named[e["proof"]] != k:
raise Stop("pop_replayed", ["record", k, "entry", j])
named.setdefault(e["proof"], k)
# Step 5.
held_hops = []
for k, d in enumerate(s["hops"]):
if d not in hops:
raise Stop("record_missing", ["hop", k])
b = hops[d]
try:
h = LMV.read_hop(b, b[5:37])
except Refuse as e:
raise Stop(e.reason, ["hop", k])
room = next((r for r in rooms if r["d"]["room"] == h["room"]), None)
if room is None:
raise Stop("record_missing", ["hop", k])
if "hop-party" not in SKIP and h["hop"] != s["party"]:
raise Stop("hop_not_party", ["hop", k])
t0 = room["d"]["t0"]
if "hop-span" not in SKIP and not (s["start"] <= t0 + h["start"] and t0 + h["end"] <= s["end"]):
raise Stop("span_outside_period", ["hop", k])
held_hops.append(h)
# Step 6.
f = figures(s, acks, held_hops)
try:
check_figures(s, f)
except Refuse as e:
raise Stop(e.reason, ["statement"])
coverage = "none" if not held_hops else ("whole" if s["unmetered"] == 0 else "partial")
return {
"outcome": "accept",
"digest": s["digest"].hex(),
"currency": None if s["currency"] == bytes(3) else s["currency"].decode("ascii"),
"classes": {CLASS_NAMES[c]: {"objects": s["classes"][c - 1][0], "bytes": s["classes"][c - 1][1]} for c in (1, 2, 3, 4)},
"lines": [{"class": CLASS_NAMES[x["class"]], "payer": x["payer"].hex(), "objects": x["objects"], "bytes": x["bytes"],
"amount": x["amount"], "free": x["class"] in RETRIEVAL and x["payer"] == ZERO} for x in s["lines"]],
"unacknowledged": {"declarations": s["unack"][0], "objects": s["unack"][1], "bytes": s["unack"][2]},
"coverage": coverage,
"unmetered": s["unmetered"],
"totals": [{"class": LABELS[t], "figures": n, "joules": j} for t, n, j in s["totals"]],
"amount": s["amount"],
}
def statement_outcome(doc, v):
try:
return verify_statement(doc, v)
except Stop as e:
return {"outcome": "refuse", "reason": e.reason, "at": e.at}
# ---------------------------------------------------------------- the corpus
def check(doc, quiet=False):
"""The failures: every case whose stated outcome is not the one this verifier reaches."""
fails = []
try:
got = {"statements": [statement_outcome(doc, v) for v in doc["statements"]],
"ranges": [range_outcome(v) for v in doc["ranges"]]}
except (Refuse, KeyError, ValueError, TypeError, IndexError, AttributeError) as e:
return ["the corpus does not read: %r" % e]
for sec in ("statements", "ranges"):
for case, out in zip(doc[sec], got[sec]):
if case.get("outcome") != out:
fails.append("%s/%s: stated %s, got %s" % (sec, case["name"], json.dumps(case.get("outcome"))[:140],
json.dumps(out)[:140]))
if not quiet:
bad = [x for x in fails if x.startswith(sec + "/")]
print(("PASS" if not bad else "FAIL") + " %s: %d cases" % (sec, len(doc[sec])))
return fails
def reasons_given(doc):
return {v["outcome"]["reason"] for sec in ("statements", "ranges") for v in doc[sec] if v["outcome"]["outcome"] == "refuse"}
def flip_hex(h, at):
c = "0" if h[at] != "0" else "1"
return h[:at] + c + h[at + 1:]
def mutations(doc):
"""Copies of the corpus, each with one defect a conforming verifier catches."""
def named(d, sec, name):
return next(c for c in d[sec] if c["name"] == name)
def m(label, f):
d = copy.deepcopy(doc)
f(d)
return label, d
def pool(d, sec, name):
return next(c for c in d[sec] if c["name"] == name)
def outcome(sec, name, key, value):
def f(d):
named(d, sec, name)["outcome"][key] = value
return f
def statement_byte(name, at):
def f(d):
x = named(d, "statements", name)
x["statement"] = flip_hex(x["statement"], at)
return f
yield m("an accepted statement's signature", statement_byte("relay_paid_by_edge_and_viewer", -6))
yield m("an accepted statement's amount", outcome("statements", "relay_paid_by_edge_and_viewer", "amount", 81))
yield m("an accepted statement's coverage", outcome("statements", "relay_unmetered_hop", "coverage", "whole"))
yield m("a line's free flag", lambda d: named(d, "statements", "edge_fetch_granted_and_free")["outcome"]["lines"][0].update(free=True))
yield m("a refused statement stated accepted", lambda d: named(d, "statements", "bills_bytes_not_acknowledged").update(
outcome={"outcome": "accept"}))
yield m("a refusal's place", outcome("statements", "chain_broken", "at", ["record", 0]))
yield m("a refusal's reason", outcome("statements", "sums_joules_across_classes", "reason", "statement_mismatch"))
yield m("a record's item list", lambda d: pool(d, "records", "relay_from_edge_1")["items"].__setitem__(
0, flip_hex(pool(d, "records", "relay_from_edge_1")["items"][0], 70)))
yield m("a record's signature", lambda d: pool(d, "records", "edge_from_viewer_a").update(
record=flip_hex(pool(d, "records", "edge_from_viewer_a")["record"], len(pool(d, "records", "edge_from_viewer_a")["record"]) - 3)))
yield m("a proof withdrawn from the pool", lambda d: d["proofs"].remove(pool(d, "proofs", "viewer_a")))
yield m("a hop record's figure", lambda d: pool(d, "hops", "h2").update(hop=flip_hex(pool(d, "hops", "h2")["hop"], 2 * 85)))
yield m("a vector's rooms", lambda d: named(d, "statements", "origin_fills_billed_to_grant").update(rooms=[]))
yield m("a vector's withheld record restored", lambda d: named(d, "statements", "acknowledgement_missing").update(withhold=[]))
yield m("a range proof's byte", lambda d: named(d, "ranges", "interior").update(
proof=flip_hex(named(d, "ranges", "interior")["proof"], len(named(d, "ranges", "interior")["proof"]) - 9)))
yield m("a range proof's stated length", outcome("ranges", "tail_establishes_length", "length_established", False))
yield m("a range proof's covered chunks", outcome("ranges", "head", "covered", [0, 2048]))
yield m("an audit's item", lambda d: named(d, "ranges", "audit_answered")["audit"].update(end=4000))
yield m("a range proof's content hash", lambda d: named(d, "ranges", "sealed_object").update(
content_hash=flip_hex(named(d, "ranges", "sealed_object")["content_hash"], 0)))
CHECKS = {
"ack-rules": "§3.6: a record's rules",
"self-ack": "§3.6: a self-acknowledgement",
"ack-signature": "§3.5: a record's signature",
"items-rules": "§3.6: an item list's rules",
"items-recompute": "§3.6: an item list recomputes",
"upstream": "§8.2 step 3.3: the record's upstream is the party",
"period": "§8.2 step 3.4: the window within the period",
"grant-window": "§6.3 step 2: the grant's window",
"grant-principal": "§6.3 step 3: the grant's principal",
"proof-checks": "§6.3 step 4: the proof",
"seek": "§6.3 step 5: the seek power",
"room-grants": "§8.2 step 2: a room's grants",
"chain-overlap": "§3.7: windows of a chain do not overlap",
"chain-prev": "§3.7: prev names the record before",
"declared": "§4.2: no byte both declared and acknowledged",
"pop-replayed": "§6.3: a proof pays for one record",
"hop-party": "§7.7: a hop record is the party's",
"hop-span": "§7.7: a hop record's span within the period",
"free-line": "§7.6 step 1: a free line bills nothing",
"bytes-covered": "§7.6 step 2: bytes no acknowledgement covers",
"classes-summed": "§7.6 step 3: a total across classes",
"figures": "§7.6 step 4: every other figure",
"statement-rules": "§7.5: a statement's rules",
"statement-signature": "§7.4: a statement's signature",
"range-bounds": "§5.3: a range within the object",
"range-hash": "§5.4 step 1: the proof's content hash",
"range-audit": "§5.4 step 2: an audit's item",
"range-exact": "§5.4 step 3: chaining values consumed exactly",
"range-root": "§5.4 step 3: the root is the content hash",
}
def main(corpus_dir):
fails = []
st = ED._self_test()
print(("PASS" if not st else "FAIL") + " Ed25519 strict rule self-test " + ",".join(st))
fails += ["ed25519 self-test"] if st else []
print(("PASS" if len(REASONS) == 19 else "FAIL") + " §11 read from the extension: %d reasons" % len(REASONS))
fails += [] if len(REASONS) == 19 else ["§11 does not read"]
with open(os.path.join(corpus_dir, "vectors.json"), encoding="utf-8") as f:
doc = json.load(f)
fails += check(doc)
extra = sorted(set(os.listdir(corpus_dir)) - {"vectors.json", "README.md"})
print(("PASS" if not extra else "FAIL") + " no file but vectors.json and README.md")
fails += ["an extra file: %s" % n for n in extra]
missing = [r for r in REASONS + REUSED if r not in reasons_given(doc)]
print(("PASS" if not missing else "FAIL") + " every reason of §11, and every code it reuses, appears in the corpus")
fails += ["no case gives %s" % r for r in missing]
for label, d in mutations(doc):
caught = check(d, quiet=True)
print(("PASS" if caught else "FAIL") + " mutated copy fails: %s%s" % (label, " (%s)" % caught[0][:90] if caught else ""))
if not caught:
fails.append("a mutated copy verifies: %s" % label)
for name, what in CHECKS.items():
SKIP.clear()
SKIP.add(name)
caught = check(doc, quiet=True)
SKIP.clear()
print(("PASS" if caught else "FAIL") + " a verifier without %s fails the corpus%s" % (what, " (%s)" % caught[0][:70] if caught else ""))
if not caught:
fails.append("the corpus does not catch a verifier without %s" % what)
for x in fails:
print("FAIL " + x)
print(("PASS" if not fails else "FAIL") + " settlement corpus: %d statements, %d range proofs" % (len(doc["statements"]), len(doc["ranges"])))
return 1 if fails else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: settlement_verify.py <live-conformance/settlement>", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))

View file

@ -266,6 +266,20 @@
"edition": null,
"registered": "2026-10-17"
},
{
"capability": "wai.delivery.settlement",
"kind": "record",
"status": "registered",
"media": null,
"role": null,
"tier": null,
"licence": null,
"replicate": null,
"sets": [],
"spec_section": "§5 Interactive, temporal & computed content",
"edition": null,
"registered": "2026-10-21"
},
{
"capability": "wai.derive.crop",
"kind": "derivation",
@ -2039,6 +2053,13 @@
"section": "§4.2",
"status": "defined"
},
{
"name": "WBR1",
"kind": "magic",
"document": "extensions/settlement.md",
"section": "§5.2",
"status": "defined"
},
{
"name": "WCD1",
"kind": "magic",
@ -2067,6 +2088,13 @@
"section": "§8.1",
"status": "defined"
},
{
"name": "WDA1",
"kind": "magic",
"document": "extensions/settlement.md",
"section": "§3.1",
"status": "defined"
},
{
"name": "WEP1",
"kind": "magic",
@ -2186,6 +2214,13 @@
"section": "§3.1",
"status": "defined"
},
{
"name": "WSS1",
"kind": "magic",
"document": "extensions/settlement.md",
"section": "§7.1",
"status": "defined"
},
{
"name": "WTM1",
"kind": "magic",

View file

@ -293,6 +293,13 @@ wai:wai.capture.take a wai:Capability ;
wai:specSection "§5 Interactive, temporal & computed content" ;
wai:registeredOn "2026-10-17"^^xsd:date .
wai:wai.delivery.settlement a wai:Capability ;
rdfs:label "wai.delivery.settlement" ;
wai:kind wai:kind-record ;
wai:status wai:status-registered ;
wai:specSection "§5 Interactive, temporal & computed content" ;
wai:registeredOn "2026-10-21"^^xsd:date .
wai:wai.derive.crop a wai:Capability ;
rdfs:label "wai.derive.crop" ;
wai:kind wai:kind-derivation ;

View file

@ -315,6 +315,15 @@ contribution = ["synthesis", "generate"]
# RFC 9651's Dictionary parser is written here. SHA-256, Ed25519 and the room's records only;
# no new dependency.
ai_usage = ["live_room"]
# Delivery settlement (extensions/settlement): `wai.delivery.settlement` — the acknowledgement a
# downstream party signs of what it received (WDA1: per object class, the objects by content hash
# under jwp-receipts §2.2's tree, their byte ranges and byte count, chained per window) and the
# declaration a hop signs of a delivery nobody acknowledged; the byte-range proof over BLAKE3's
# chunk tree (WBR1, through blake3's hazmat API, as the HTTP head proof); paid retrieval billed only
# to a grant whose WRP1 proof the request carried; and the settlement statement (WSS1) whose lines,
# declared bytes, per-class joules from live-metering's WMH1 and integer amount a verifier
# recomputes. SHA-256, BLAKE3, Ed25519 and the room's records only; no new dependency.
settlement = ["live_metering", "jwp_hash"]
# Timed text and live captions (extensions/timed-text): `wai.text.timed` — the cue model,
# live caption revisions (`caption` events of wai.live.events with the WTX1 annotation),
# and their exact mappings onto WebVTT documents and segments, the IMSC1 Text Profile,
@ -812,6 +821,11 @@ name = "wai_ai_usage_vectors"
path = "src/bin/wai_ai_usage_vectors.rs"
required-features = ["ai_usage"]
[[bin]]
name = "wai_settlement_vectors"
path = "src/bin/wai_settlement_vectors.rs"
required-features = ["settlement"]
[[bin]]
name = "wai_timed_text_vectors"
path = "src/bin/wai_timed_text_vectors.rs"
@ -1537,6 +1551,11 @@ name = "ai_usage"
path = "tests/ai_usage.rs"
required-features = ["ai_usage"]
[[test]]
name = "settlement"
path = "tests/settlement.rs"
required-features = ["settlement"]
[[test]]
name = "timed_text"
path = "tests/timed_text.rs"

View file

@ -0,0 +1,55 @@
//! Conformance corpus for delivery settlement (`live-conformance/settlement/`,
//! extensions/settlement.md §12).
//!
//! wai_settlement_vectors gen <corpus-dir> write vectors.json, from fixed seeds
//! wai_settlement_vectors verify <corpus-dir> vectors.json regenerates byte for byte,
//! and every statement and range proof gets
//! the outcome it states
//!
//! The generator and this verifier are one implementation. The corpus's second
//! implementation is `live-conformance/settlement_verify.py`, written from the
//! extension's text and sharing no code with this binary.
use std::path::Path;
use wai::settlement::corpus;
fn main() {
let a: Vec<String> = std::env::args().collect();
match (a.get(1).map(String::as_str), a.get(2)) {
(Some("gen"), Some(d)) => {
let p = Path::new(d).join("vectors.json");
if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, corpus::generate())) {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
}
println!("wrote {}", p.display());
}
(Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
_ => {
eprintln!("usage: wai_settlement_vectors gen|verify <corpus-dir>");
std::process::exit(2);
}
}
}
fn verify(dir: &Path) -> i32 {
let p = dir.join("vectors.json");
let doc = match std::fs::read_to_string(&p) {
Ok(s) => s,
Err(e) => {
eprintln!("read {}: {e}", p.display());
return 1;
}
};
let mut fail = 0;
let same = doc == corpus::generate();
println!("{} vectors.json regenerates byte for byte from its seeds", if same { "PASS" } else { "FAIL" });
fail += (!same) as i32;
let fails = corpus::verify(&doc);
for f in &fails {
println!("FAIL {f}");
}
println!("{} every statement and range proof gets the outcome the corpus states", if fails.is_empty() { "PASS" } else { "FAIL" });
fail += (!fails.is_empty()) as i32;
(fail != 0) as i32
}

View file

@ -226,6 +226,7 @@ pub const CAP_EDGE_DERIVATION: &str = "wai.edge.derivation";
pub const CAP_MEDIA_SYNTHESIS: &str = "wai.media.synthesis";
pub const CAP_AI_USAGE: &str = "wai.ai.usage";
pub const CAP_MEDIA_CONTRIBUTION: &str = "wai.media.contribution";
pub const CAP_DELIVERY_SETTLEMENT: &str = "wai.delivery.settlement";
/// One row per string SPEC §5 registers that is a payload, companion or record:
/// `(string, SPEC section, edition, registered)`.
@ -320,6 +321,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[
(CAP_MEDIA_SYNTHESIS, S_INTERACTIVE, None, "2026-10-21"),
(CAP_AI_USAGE, S_INTERACTIVE, None, "2026-10-21"),
(CAP_MEDIA_CONTRIBUTION, S_INTERACTIVE, None, "2026-10-21"),
(CAP_DELIVERY_SETTLEMENT, S_INTERACTIVE, None, "2026-10-21"),
(CAP_AUDIO_IAMF, S_IMMERSIVE, None, "2026-06-17"),
(CAP_AUDIO_AC4, S_IMMERSIVE, None, "2026-06-17"),
(CAP_AUDIO_IVAS, S_IMMERSIVE, None, "2026-06-17"),
@ -377,6 +379,7 @@ const RECORDS: &[&str] = &[
CAP_MEDIA_SYNTHESIS,
CAP_AI_USAGE,
CAP_MEDIA_CONTRIBUTION,
CAP_DELIVERY_SETTLEMENT,
];
/// A named identifier SPEC uses that has the shape of a capability string but is
@ -538,6 +541,7 @@ const LIVE_PROVENANCE: &str = "extensions/live-provenance.md";
const DERIVING_EDGE: &str = "extensions/deriving-edge.md";
const SYNTHESIS: &str = "extensions/synthesis.md";
const CONTRIBUTION: &str = "extensions/contribution.md";
const SETTLEMENT: &str = "extensions/settlement.md";
const fn wire(name: &'static str, kind: WireKind, document: &'static str, section: &'static str,
status: WireStatus) -> WireIdentifier {
@ -549,8 +553,8 @@ const fn wire(name: &'static str, kind: WireKind, document: &'static str, sectio
/// (jwp-receipts §7), of packaging, of live events, of live rooms, of capture binding, of
/// live composition, of live recording, of timed text, of live metering, of energy steering,
/// of live moderation, of confidentiality, of forensic watermark sessions, of live ads, of live
/// provenance, of the deriving edge, of synthesis, of AI usage and of contribution records, in
/// document order. The export lists them sorted by name and kind.
/// provenance, of the deriving edge, of synthesis, of AI usage, of contribution records and of
/// delivery settlement, in document order. The export lists them sorted by name and kind.
pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("WAI1", WireKind::Magic, "SPEC.md", "§2", WireStatus::Defined),
wire("WAI2", WireKind::Magic, "SPEC.md", "§7.1", WireStatus::Defined),
@ -654,6 +658,9 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[
wire("wai-step", WireKind::DocumentKind, CONTRIBUTION, "§4.1", WireStatus::Defined),
wire("wai-contribution", WireKind::DocumentKind, CONTRIBUTION, "§6", WireStatus::Defined),
wire("wai-authorship-map", WireKind::DocumentKind, CONTRIBUTION, "§7.3", WireStatus::Defined),
wire("WDA1", WireKind::Magic, SETTLEMENT, "§3.1", WireStatus::Defined),
wire("WBR1", WireKind::Magic, SETTLEMENT, "§5.2", WireStatus::Defined),
wire("WSS1", WireKind::Magic, SETTLEMENT, "§7.1", WireStatus::Defined),
];
/// A string with the shape of a capability that the reference implementation uses

View file

@ -526,6 +526,15 @@ pub mod derive_edge;
/// cbindgen:ignore
#[cfg(feature = "ai_usage")]
pub mod ai_usage;
// Delivery settlement (extensions/settlement.md): `wai.delivery.settlement` — the
// acknowledgement a downstream party signs of what it received, the declaration of a delivery
// nobody acknowledged, the byte-range proof over BLAKE3's chunk tree, paid retrieval billed only
// to a grant with its proof, and the settlement statement a verifier recomputes from the
// acknowledgements. Rust-only, like the live modules: its constants must not become macros in
// include/wai.h.
/// cbindgen:ignore
#[cfg(feature = "settlement")]
pub mod settlement;
// Timed text and live captions (extensions/timed-text.md): `wai.text.timed` — the cue model
// both WebVTT and the IMSC1 Text Profile express, live caption revisions as `caption`
// events of `wai.live.events` with the WTX1 annotation, and the exact mappings onto WebVTT

1511
wai/wai-rs/src/settlement.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,765 @@
//! The conformance corpus of extensions/settlement.md §12: `live-conformance/settlement/vectors.json`.
//!
//! [`generate`] writes it from fixed seeds: every key is SHA-256 of this corpus's label and an
//! index, every object's bytes a SHA-256 stream of another label and its name, every proof's
//! challenge SHA-256 of a third label and an index, every time a constant, and Ed25519 signing
//! is deterministic (RFC 8032). The objects are opaque test bytes standing for envelopes, which
//! nothing parses; the byte counts, prices and joule figures are synthetic constants chosen to
//! exercise the rules, and claim nothing about any deployment. Each vector states the outcome it
//! was written to have, and the generator asserts that this module reaches it.
//!
//! [`verify`] checks a corpus file against this module. `live-conformance/settlement_verify.py`
//! checks the same file from the extension's text, sharing no code with this module.
use std::collections::BTreeMap;
use super::*;
use crate::energy_class::{CounterFiltering, EnergyProvenance, Uncertainty};
use crate::live_events::{kind as ek, Policy as EventPolicy};
use crate::live_room::{Caps, Grant, Limits, Recording, Review, Role, NO_CEILING};
use serde_json::{json, Map, Value};
const S: u64 = 1_000_000;
/// The room's `t0`: 2026-10-16T00:00:00Z, as the live corpora's.
const T0_US: u64 = 1_792_108_800_000_000;
const T0_S: u64 = T0_US / S;
/// The period every statement settles: the room's first minute.
const P_START: u64 = T0_US;
const P_END: u64 = T0_US + 60 * S;
const AUTHORITY: u8 = 0;
const CLOCK: u8 = 1;
const ORIGIN: u8 = 2;
const RELAY: u8 = 3;
const EDGE: u8 = 4;
const VIEWER_A: u8 = 5;
const VIEWER_B: u8 = 6;
const VIEWER_C: u8 = 7;
const FORWARDER: u8 = 8;
const VIEWER_D: u8 = 9;
const STRANGER: u8 = 10;
const PRINCIPALS: [(&str, u8); 11] = [
("authority", AUTHORITY),
("clock", CLOCK),
("origin", ORIGIN),
("relay", RELAY),
("edge", EDGE),
("viewer_a", VIEWER_A),
("viewer_b", VIEWER_B),
("viewer_c", VIEWER_C),
("forwarder", FORWARDER),
("viewer_d", VIEWER_D),
("stranger", STRANGER),
];
const ZERO: [u8; 32] = [0; 32];
fn key(i: u8) -> SigningKey {
SigningKey::from_bytes(&sha256(&[b"wai:settlement/corpus/key\x00", &[i]]))
}
fn pk(i: u8) -> [u8; 32] {
key(i).verifying_key().to_bytes()
}
fn challenge(i: u8) -> [u8; 32] {
sha256(&[b"wai:settlement/corpus/challenge\x00", &[i]])
}
fn unhex(s: &str) -> Option<Vec<u8>> {
if !s.len().is_multiple_of(2) {
return None;
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(s.get(i..i + 2)?, 16).ok()).collect()
}
/// `n` opaque bytes named `name`: a SHA-256 stream.
fn object(name: &str, n: usize) -> Vec<u8> {
let mut o = Vec::with_capacity(n + 32);
let mut i = 0u32;
while o.len() < n {
o.extend_from_slice(&sha256(&[b"wai:settlement/corpus/object\x00", name.as_bytes(), b"\x00", &i.to_le_bytes()]));
i += 1;
}
o.truncate(n);
o
}
/// The objects: name, length. `s*` stand for sealed objects (confidentiality §8.1): their
/// bytes are opaque as ciphertext is.
const OBJECTS: [(&str, usize); 12] = [
("o1", 600),
("o2", 1_500),
("o3", 2_100),
("o4", 3_000),
("o5", 400),
("o6", 9_000),
("o7", 20_000),
("o8", 5_121),
("o6_twin", 9_000),
("s1", 1_800),
("s2", 5_000),
("s3", 12_000),
];
fn obj(name: &str) -> Vec<u8> {
let n = OBJECTS.iter().find(|(o, _)| *o == name).unwrap_or_else(|| panic!("no object {name}")).1;
object(name, n)
}
fn whole(name: &str) -> Item {
Item::whole(&obj(name))
}
fn part(name: &str, start: u64, end: u64) -> Item {
Item { content_hash: content_hash(&obj(name)), start, end }
}
// ---- the room ----
fn bits(ks: &[u8]) -> [u8; 32] {
let mut b = [0u8; 32];
for &k in ks {
b[(k / 8) as usize] |= 1 << (k % 8);
}
b
}
fn descriptor() -> Descriptor {
let role = |name: &str, powers: u8| Role { name: name.into(), caps: Caps { publish: 0, subscribe: true, max_layer: 1, powers }, events: bits(&[ek::CHAT, ek::REACTION]) };
Descriptor {
authority: pk(AUTHORITY),
clock: pk(CLOCK),
t0_us: T0_US,
tick_us: S,
origin: String::new(),
limits: Limits { max_present: 64, max_publishers: 2, reval_s: 0, ceiling: EventPolicy::default() },
roles: vec![role("audience_seek", room::power::SEEK), role("audience", 0)],
slots: vec![],
profiles: vec![],
review: Review { kinds: [0; 32], deadline_ms: 0, fallback: 0 },
recording: Recording { mode: 0, retention_days: 0, commit_interval_ms: 0, jitter_ms: 0, regions: vec![] },
sig: [0; 64],
}
.sign(&key(AUTHORITY))
}
fn grant(room_id: [u8; 32], who: u8, role_index: u8, exp: u64) -> Grant {
let d = descriptor();
Grant {
room: room_id,
principal: pk(who),
prev: [0; 32],
effective: 0,
nbf: T0_S - 3_600,
exp,
role: role_index,
caps: d.roles[role_index as usize].caps,
precedence: 0,
once: [0; 16],
viewer: [0; 16],
policy: EventPolicy::with_kinds(&[ek::CHAT, ek::REACTION]),
joules_ceiling: NO_CEILING,
funds_ceiling: NO_CEILING,
currency: [0; 3],
profiles: vec![],
origins: vec!["https://edge.example".into()],
attrs: vec![],
sig: [0; 64],
}
}
/// The room's grants, in order: viewer A's with the seek power, viewer B's, C's and D's
/// without it, and viewer A's second grant, which expires 15 s into the room.
struct Grants {
ga: [u8; 32],
gb: [u8; 32],
gc: [u8; 32],
gd: [u8; 32],
gx: [u8; 32],
records: Vec<Vec<u8>>,
}
fn grants() -> Grants {
let id = descriptor().room_id();
let day = T0_S + 86_400;
let gs = [grant(id, VIEWER_A, 0, day), grant(id, VIEWER_B, 1, day), grant(id, VIEWER_C, 1, day), grant(id, VIEWER_D, 1, day), grant(id, VIEWER_A, 1, T0_S + 15)];
let signed: Vec<Grant> = gs.into_iter().map(|g| g.sign(&key(AUTHORITY))).collect();
Grants {
ga: signed[0].digest(),
gb: signed[1].digest(),
gc: signed[2].digest(),
gd: signed[3].digest(),
gx: signed[4].digest(),
records: signed.iter().map(Grant::to_bytes).collect(),
}
}
/// A grant for the stranger that the stranger signed, which a room's grants refuse.
fn forged_grant() -> Vec<u8> {
grant(descriptor().room_id(), STRANGER, 1, T0_S + 86_400).sign(&key(STRANGER)).to_bytes()
}
// ---- the proofs ----
struct Proofs {
pa: Vec<u8>,
pb: Vec<u8>,
wrong_key: Vec<u8>,
bad_sig: Vec<u8>,
wrong_grant: Vec<u8>,
malformed: Vec<u8>,
}
fn proofs(g: &Grants) -> Proofs {
let ctx = room::proof_context("SETUP", "edge.example", "/moq");
let pa = Proof::sign(g.ga, &key(VIEWER_A), challenge(1), &ctx).to_bytes();
let pb = Proof::sign(g.gb, &key(VIEWER_B), challenge(2), &ctx).to_bytes();
let wrong_key = Proof::sign(g.ga, &key(STRANGER), challenge(3), &ctx).to_bytes();
let mut bad = Proof::sign(g.ga, &key(STRANGER), challenge(4), &ctx);
bad.key = pk(VIEWER_A);
let wrong_grant = Proof::sign(g.gb, &key(VIEWER_A), challenge(5), &ctx).to_bytes();
let mut malformed = Proof::sign(g.ga, &key(VIEWER_A), challenge(6), &ctx).to_bytes();
malformed.pop();
Proofs { pa, pb, wrong_key, bad_sig: bad.to_bytes(), wrong_grant, malformed }
}
// ---- the records ----
/// A record, its bytes and its item lists.
#[derive(Clone)]
struct Rec {
name: String,
ack: Ack,
items: Vec<Vec<u8>>,
}
impl Rec {
fn digest(&self) -> [u8; 32] {
self.ack.digest()
}
fn bytes(&self) -> Vec<u8> {
self.ack.to_bytes()
}
}
type EntrySpec = (u8, [u8; 32], [u8; 32], Vec<Item>);
#[allow(clippy::too_many_arguments)]
fn rec(name: &str, m: u8, rl: u8, up: u8, down: Option<u8>, w: (u64, u64), prev: [u8; 32], entries: Vec<EntrySpec>) -> Rec {
let mut es = entries;
es.sort_by_key(|(c, g, p, _)| (*c, *g, *p));
let a = Ack {
mode: m,
role: rl,
upstream: pk(up),
downstream: down.map_or([0; 32], pk),
start: T0_US + w.0 * S,
end: T0_US + w.1 * S,
prev,
entries: es.iter().map(|(c, g, p, items)| Entry::of(*c, *g, *p, items)).collect(),
sig: [0; 64],
};
let signer = if m == mode::DECLARATION { up } else { down.expect("an acknowledgement names its signer") };
Rec { name: name.into(), ack: a.sign(&key(signer)), items: es.iter().map(|(_, _, _, items)| item_list(items)).collect() }
}
/// An acknowledgement whose window is given in microseconds, for one outside the room's
/// first minute.
#[allow(clippy::too_many_arguments)]
fn rec_at(name: &str, m: u8, rl: u8, up: u8, down: u8, start: u64, end: u64, entries: Vec<EntrySpec>) -> Rec {
let mut r = rec(name, m, rl, up, Some(down), (0, 1), ZERO, entries);
r.ack.start = start;
r.ack.end = end;
r.ack = r.ack.clone().sign(&key(down));
r
}
fn live(items: Vec<Item>) -> EntrySpec {
(class::LIVE, ZERO, ZERO, items)
}
struct World {
g: Grants,
p: Proofs,
recs: BTreeMap<&'static str, Rec>,
hops: BTreeMap<&'static str, HopRecord>,
}
fn hop(who: u8, start_s: u64, end_s: u64, joules: u64, class: Option<EnergyProvenance>) -> HopRecord {
HopRecord {
room: descriptor().room_id(),
hop: pk(who),
role: 1,
start: start_s * S,
end: end_s * S,
joules,
class,
tracks: vec![],
sessions: 0,
subscriber_us: [0; metering::CLASSES],
events: 0,
grants: 0,
ledger: [0; 32],
dvr: None,
sig: [0; 64],
}
.sign(&key(who))
}
fn world() -> World {
let g = grants();
let p = proofs(&g);
let pa = proof_digest(&p.pa);
let mut recs: BTreeMap<&'static str, Rec> = BTreeMap::new();
let mut put = |k: &'static str, r: Rec| {
recs.insert(k, r);
};
use mode::{ACK, DECLARATION as DEC};
use role::{HOP as H, SINK as K};
// The relay's.
let a1 = rec("relay_from_edge_1", ACK, H, RELAY, Some(EDGE), (0, 20), ZERO, vec![live(vec![whole("o1"), whole("o2"), whole("o3"), whole("o4")]), (class::ANCILLARY, ZERO, ZERO, vec![whole("o5")])]);
let a2 = rec("relay_from_edge_2", ACK, H, RELAY, Some(EDGE), (20, 40), a1.digest(), vec![live(vec![whole("o6"), whole("o7")])]);
let a3 = rec("relay_from_viewer_a", ACK, K, RELAY, Some(VIEWER_A), (0, 30), ZERO, vec![(class::LIVE, g.ga, ZERO, vec![whole("o1"), whole("o2"), whole("o3")])]);
let d1 = rec("relay_declared_viewer_d", DEC, K, RELAY, Some(VIEWER_D), (0, 30), ZERO, vec![(class::LIVE, g.gd, ZERO, vec![whole("o1"), whole("o2"), whole("o3"), whole("o4")])]);
let d2 = rec("relay_declared_keyless", DEC, K, RELAY, None, (0, 60), ZERO, vec![live(vec![whole("o1"), whole("o2")])]);
put("a0", rec_at("relay_from_edge_previous_period", ACK, H, RELAY, EDGE, T0_US - 20 * S, T0_US, vec![live(vec![whole("o1")])]));
put("a2_overlap", rec("relay_from_edge_overlapping", ACK, H, RELAY, Some(EDGE), (15, 35), a1.digest(), vec![live(vec![whole("o6")])]));
put("a2_unchained", rec("relay_from_edge_unchained", ACK, H, RELAY, Some(EDGE), (20, 40), ZERO, vec![live(vec![whole("o6")])]));
put("a_self", rec("relay_from_itself", ACK, H, RELAY, Some(RELAY), (0, 30), ZERO, vec![live(vec![whole("o1")])]));
put("a_invalid", rec("relay_fetch_without_proof", ACK, K, RELAY, Some(VIEWER_A), (30, 60), ZERO, vec![(class::FETCH, g.ga, ZERO, vec![whole("o6")])]));
put("d3", rec("relay_declared_viewer_a", DEC, K, RELAY, Some(VIEWER_A), (0, 30), ZERO, vec![live(vec![whole("o1")])]));
// The edge's.
let b1 = rec("edge_from_viewer_a", ACK, K, EDGE, Some(VIEWER_A), (0, 30), ZERO, vec![(class::LIVE, g.ga, ZERO, vec![whole("o1")]), (class::FETCH, g.ga, pa, vec![whole("o6"), part("o7", 0, 8_192)])]);
let b2 = rec("edge_from_viewer_b", ACK, K, EDGE, Some(VIEWER_B), (0, 30), ZERO, vec![(class::LIVE, g.gb, ZERO, vec![whole("o2"), whole("o3")]), (class::FETCH, ZERO, ZERO, vec![whole("o4")])]);
let later = |name: &str, entry: EntrySpec| rec(name, ACK, K, EDGE, Some(VIEWER_A), (30, 60), b1.digest(), vec![entry]);
put("b3", rec("edge_fetch_viewer_b_no_seek", ACK, K, EDGE, Some(VIEWER_B), (30, 60), b2.digest(), vec![(class::FETCH, g.gb, proof_digest(&p.pb), vec![whole("o6")])]));
put("b4", later("edge_fetch_proof_wrong_key", (class::FETCH, g.ga, proof_digest(&p.wrong_key), vec![whole("o6")])));
put("b5", later("edge_fetch_proof_bad_signature", (class::FETCH, g.ga, proof_digest(&p.bad_sig), vec![whole("o6")])));
put("b6", later("edge_fetch_proof_wrong_grant", (class::FETCH, g.ga, proof_digest(&p.wrong_grant), vec![whole("o6")])));
put("b7", later("edge_fetch_proof_malformed", (class::FETCH, g.ga, proof_digest(&p.malformed), vec![whole("o6")])));
put("b8", later("edge_fetch_proof_again", (class::FETCH, g.ga, pa, vec![whole("o8")])));
put("b9", later("edge_live_other_grant", (class::LIVE, g.gb, ZERO, vec![whole("o2")])));
put("b10", rec("edge_live_expired_grant", ACK, K, EDGE, Some(VIEWER_A), (10, 20), ZERO, vec![(class::LIVE, g.gx, ZERO, vec![whole("o1")])]));
put("b11", rec("edge_ranges_overlapping", ACK, K, EDGE, Some(VIEWER_A), (30, 60), b1.digest(), vec![live(vec![part("o7", 0, 8_192), part("o7", 4_096, 9_000)])]));
// The origin's: the edge's fills.
let c1 = rec("origin_from_edge", ACK, H, ORIGIN, Some(EDGE), (0, 30), ZERO, vec![(class::FILL, ZERO, ZERO, vec![whole("o8")]), (class::FILL, g.ga, pa, vec![whole("o6"), whole("o7")])]);
// The forwarder's: sealed objects to viewer C.
let e1 = rec("forwarder_from_viewer_c", ACK, K, FORWARDER, Some(VIEWER_C), (0, 60), ZERO, vec![(class::LIVE, g.gc, ZERO, vec![whole("s1"), whole("s2"), whole("s3")])]);
put("a1", a1);
put("a2", a2);
put("a3", a3);
put("d1", d1);
put("d2", d2);
put("b1", b1);
put("b2", b2);
put("c1", c1);
put("e1", e1);
let u = |ppm| Uncertainty::new(ppm, 30 * S).expect("a declaration");
let counter = EnergyProvenance::on_chip_counter(u(20_000), CounterFiltering::Off).expect("a class");
let mut hops = BTreeMap::new();
hops.insert("h1", hop(RELAY, 0, 30, 4_000_000, Some(counter)));
hops.insert("h2", hop(RELAY, 30, 60, 3_000_000, Some(EnergyProvenance::ModelBased)));
hops.insert("h3", hop(RELAY, 30, 60, 0, None));
hops.insert("h_edge", hop(EDGE, 0, 30, 2_000_000, Some(EnergyProvenance::ModelBased)));
hops.insert("h_late", hop(RELAY, 50, 70, 1_000_000, Some(EnergyProvenance::ModelBased)));
hops.insert("h_energy", hop(RELAY, 0, 30, 0, Some(EnergyProvenance::ModelBased)));
World { g, p, recs, hops }
}
fn usd() -> Tariff {
Tariff { currency: *b"USD", prices: [2_000_000, 5_000_000, 8_000_000, 1_000_000] }
}
impl World {
fn r(&self, k: &str) -> &Rec {
self.recs.get(k).unwrap_or_else(|| panic!("no record {k}"))
}
fn h(&self, k: &str) -> &HopRecord {
self.hops.get(k).unwrap_or_else(|| panic!("no hop record {k}"))
}
/// The statement of `party` over records `rs` and hop records `hs`, computed and unsigned.
fn compute(&self, party: u8, rs: &[&str], hs: &[&str], t: Tariff) -> Statement {
let records: Vec<([u8; 32], Ack)> = rs.iter().map(|k| (self.r(k).digest(), self.r(k).ack.clone())).collect();
let hops: Vec<([u8; 32], HopRecord)> = hs.iter().map(|k| (self.h(k).digest(), self.h(k).clone())).collect();
compute(pk(party), P_START, P_END, &records, &hops, t)
}
}
// ---- the vectors ----
/// What a vector gives beside the pool: withheld digests, extra records with their item
/// lists, extra proofs and hop records, and the rooms by name.
#[derive(Default)]
struct Given {
withhold: Vec<[u8; 32]>,
extra_records: Vec<(Vec<u8>, Vec<Vec<u8>>)>,
extra_proofs: Vec<Vec<u8>>,
/// The rooms given, by name; `None` for the lesson room alone.
rooms: Option<Vec<&'static str>>,
}
struct Vector {
name: &'static str,
statement: Vec<u8>,
given: Given,
expect: &'static str,
}
fn sign_as(s: Statement, who: u8) -> Vec<u8> {
s.sign(&key(who)).to_bytes()
}
fn vectors(w: &World) -> Vec<Vector> {
let mut v = Vec::new();
let mut add = |name: &'static str, statement: Vec<u8>, given: Given, expect: &'static str| {
v.push(Vector { name, statement, given, expect });
};
let relay_records = ["a1", "a2", "a3", "d1", "d2"];
let relay = || w.compute(RELAY, &relay_records, &["h1", "h2"], usd());
let edge = || w.compute(EDGE, &["b1", "b2"], &[], usd());
let g = Given::default;
// Accepted.
add("relay_paid_by_edge_and_viewer", sign_as(relay(), RELAY), g(), "accept");
add("edge_fetch_granted_and_free", sign_as(edge(), EDGE), g(), "accept");
add("origin_fills_billed_to_grant", sign_as(w.compute(ORIGIN, &["c1"], &[], usd()), ORIGIN), g(), "accept");
add("viewer_forwards_sealed_objects", sign_as(w.compute(FORWARDER, &["e1"], &[], Tariff::none()), FORWARDER), g(), "accept");
add("relay_unmetered_hop", sign_as(w.compute(RELAY, &["a1"], &["h1", "h3"], usd()), RELAY), g(), "accept");
// Refused: the figures.
let mut s = relay();
s.lines[0].bytes += 1_000;
add("bills_bytes_not_acknowledged", sign_as(s, RELAY), g(), "bytes_not_acknowledged");
let mut s = relay();
let mut extra = s.lines[0];
extra.payer = pk(STRANGER);
s.lines.push(extra);
s.lines.sort_by_key(|l| (l.kind, l.payee, l.class, l.payer));
add("bills_a_party_that_signed_nothing", sign_as(s, RELAY), g(), "bytes_not_acknowledged");
let mut s = edge();
let free = s.lines.iter_mut().find(|l| l.is_free()).expect("a free line");
free.amount = usd().amount(free.class, free.bytes as u128) as u64;
s.amount += free.amount;
add("bills_a_fetch_without_grant", sign_as(s, EDGE), g(), "paid_without_grant");
let mut s = relay();
let all: u64 = s.totals.iter().map(|t| t.joules).sum();
let figures: u32 = s.totals.iter().map(|t| t.figures).sum();
s.totals = vec![Total { tag: 4, figures, joules: all }];
add("sums_joules_across_classes", sign_as(s, RELAY), g(), "classes_summed");
let mut s = relay();
s.totals = vec![Total { tag: 0, figures, joules: all }];
add("totals_joules_of_no_class", sign_as(s, RELAY), g(), "classes_summed");
let mut s = relay();
s.amount += 1;
add("amount_does_not_recompute", sign_as(s, RELAY), g(), "statement_mismatch");
let mut s = relay();
s.unacknowledged = (s.unacknowledged.0, 0, 0);
add("declared_bytes_stated_as_zero", sign_as(s, RELAY), g(), "statement_mismatch");
let mut s = relay();
s.lines[0].bytes -= 1;
add("bills_fewer_bytes_than_acknowledged", sign_as(s, RELAY), g(), "statement_mismatch");
let mut s = relay();
s.lines.push(Line { kind: line_kind::RESERVED_ATTRIBUTION, payee: pk(STRANGER), class: class::LIVE, payer: pk(RELAY), objects: 1, bytes: 1, amount: 0 });
add("attribution_line_reserved", sign_as(s, RELAY), g(), "statement_invalid");
// Refused: the records.
add("acknowledgement_replayed_from_another_period", sign_as(w.compute(RELAY, &["a1", "a0"], &[], usd()), RELAY), g(), "ack_outside_period");
add("acknowledgement_signed_by_the_hop_itself", sign_as(w.compute(RELAY, &["a1", "a_self"], &[], usd()), RELAY), g(), "self_acknowledged");
add("windows_overlap", sign_as(w.compute(RELAY, &["a1", "a2_overlap"], &[], usd()), RELAY), g(), "ack_overlap");
add("chain_broken", sign_as(w.compute(RELAY, &["a1", "a2_unchained"], &[], usd()), RELAY), g(), "ack_chain_broken");
add("declared_and_acknowledged", sign_as(w.compute(RELAY, &["a3", "d3"], &[], usd()), RELAY), g(), "declared_and_acknowledged");
add("another_party_s_acknowledgement", sign_as(w.compute(RELAY, &["a1", "b1"], &[], usd()), RELAY), g(), "ack_wrong_party");
add("retrieval_with_grant_and_no_proof", sign_as(w.compute(RELAY, &["a_invalid"], &[], usd()), RELAY), g(), "ack_invalid");
let a1 = w.r("a1");
let mut short = a1.items.clone();
short[0].pop();
add("items_do_not_read", sign_as(relay(), RELAY), Given { withhold: vec![a1.digest()], extra_records: vec![(a1.bytes(), short)], ..g() }, "items_invalid");
let mut changed = a1.items.clone();
let at = changed[0].len() - 1;
changed[0][at - 7] ^= 1;
add("items_share_a_byte", sign_as(w.compute(EDGE, &["b11"], &[], usd()), EDGE), g(), "items_invalid");
add("items_do_not_recompute", sign_as(relay(), RELAY), Given { withhold: vec![a1.digest()], extra_records: vec![(a1.bytes(), changed)], ..g() }, "items_mismatch");
let mut flipped = a1.bytes();
let last = flipped.len() - 1;
flipped[last] ^= 1;
add("acknowledgement_signature", sign_as(relay(), RELAY), Given { withhold: vec![a1.digest()], extra_records: vec![(flipped, a1.items.clone())], ..g() }, "bad_signature");
add("acknowledgement_missing", sign_as(relay(), RELAY), Given { withhold: vec![w.r("a2").digest()], ..g() }, "record_missing");
add("items_missing", sign_as(relay(), RELAY), Given { withhold: vec![a1.digest()], extra_records: vec![(a1.bytes(), vec![])], ..g() }, "record_missing");
// Refused: grants and proofs.
add("proof_missing", sign_as(edge(), EDGE), Given { withhold: vec![proof_digest(&w.p.pa)], ..g() }, "record_missing");
add("room_missing", sign_as(edge(), EDGE), Given { rooms: Some(vec![]), ..g() }, "record_missing");
add("room_grant_forged", sign_as(edge(), EDGE), Given { rooms: Some(vec!["lesson_forged"]), ..g() }, "bad_signature");
let edge_one = |k: &str| sign_as(w.compute(EDGE, &[k], &[], usd()), EDGE);
add("fetch_without_seek_power", edge_one("b3"), g(), "seek_not_granted");
add("proof_by_another_key", edge_one("b4"), g(), "pop_wrong_key");
add("proof_signature", edge_one("b5"), g(), "pop_bad_signature");
add("proof_names_another_grant", edge_one("b6"), g(), "pop_wrong_grant");
add("proof_does_not_read", edge_one("b7"), g(), "pop_malformed");
add("proof_presented_twice", sign_as(w.compute(EDGE, &["b1", "b8"], &[], usd()), EDGE), g(), "pop_replayed");
add("grant_of_another_principal", edge_one("b9"), g(), "grant_wrong_principal");
add("grant_expired_in_window", edge_one("b10"), g(), "outside_grant_window");
// Refused: hop records.
add("hop_record_missing", sign_as(relay(), RELAY), Given { withhold: vec![w.h("h2").digest()], ..g() }, "record_missing");
add("hop_record_of_another_party", sign_as(w.compute(RELAY, &["a1"], &["h1", "h_edge"], usd()), RELAY), g(), "hop_not_party");
add("hop_record_outside_period", sign_as(w.compute(RELAY, &["a1"], &["h1", "h_late"], usd()), RELAY), g(), "span_outside_period");
add("hop_record_energy", sign_as(w.compute(RELAY, &["a1"], &["h1", "h_energy"], usd()), RELAY), g(), "energy_invalid");
// Refused: the statement.
let mut b = sign_as(relay(), RELAY);
let last = b.len() - 1;
b[last] ^= 1;
add("statement_signature", b, g(), "bad_signature");
let mut s = relay();
s.party = {
let mut k = [0u8; 32];
k[0] = 1;
k
};
for l in &mut s.lines {
l.payee = s.party;
}
add("statement_party_small_order", sign_as(s, RELAY), g(), "bad_key");
let b = sign_as(relay(), RELAY);
add("statement_truncated", b[..b.len() - 1].to_vec(), g(), "truncated");
let mut b = sign_as(relay(), RELAY);
b.push(0);
add("statement_trailing_byte", b, g(), "malformed");
let mut b = sign_as(relay(), RELAY);
b[4] = 2;
add("statement_version", b, g(), "unknown_version");
let mut s = relay();
s.records.reverse();
add("statement_records_unordered", sign_as(s, RELAY), g(), "statement_invalid");
v
}
/// A range vector: a proof, the content hash it is checked against, the item of an audit it
/// answers, and the outcome it was written to have.
struct RangeVector {
name: &'static str,
proof: Vec<u8>,
hash: [u8; 32],
audit: Option<Item>,
expect: &'static str,
}
fn range_vectors() -> Vec<RangeVector> {
let mut v = Vec::new();
let mut add = |name: &'static str, proof: Vec<u8>, hash: [u8; 32], audit: Option<Item>, expect: &'static str| {
v.push(RangeVector { name, proof, hash, audit, expect });
};
let (o7, o6, o1, s2) = (obj("o7"), obj("o6"), obj("o1"), obj("s2"));
let (h7, h6) = (content_hash(&o7), content_hash(&o6));
let pr = |o: &[u8], a: u64, b: u64| RangeProof::build(o, a, b);
add("interior", pr(&o7, 3_000, 7_000).to_bytes(), h7, None, "accept");
add("head", pr(&o7, 0, 500).to_bytes(), h7, None, "accept");
add("tail_establishes_length", pr(&o7, 18_000, 20_000).to_bytes(), h7, None, "accept");
add("within_one_chunk", pr(&o7, 2_100, 2_200).to_bytes(), h7, None, "accept");
add("whole_object_of_one_chunk", pr(&o1, 0, 600).to_bytes(), content_hash(&o1), None, "accept");
add("whole_object_of_several_chunks", pr(&o6, 0, 9_000).to_bytes(), h6, None, "accept");
add("sealed_object", pr(&s2, 1_000, 4_000).to_bytes(), content_hash(&s2), None, "accept");
add("audit_answered", pr(&o7, 0, 4_096).to_bytes(), h7, Some(part("o7", 0, 8_192)), "accept");
let mut p = pr(&o7, 3_000, 7_000);
p.envelope_len = 19_950;
add("length_not_established", p.to_bytes(), h7, None, "accept");
// Refused.
let mut p = pr(&o7, 18_000, 20_000);
p.end = 20_001;
add("past_the_end", p.to_bytes(), h7, None, "range_outside_object");
let mut p = pr(&o7, 3_000, 7_000);
p.end = p.start;
add("empty_range", p.to_bytes(), h7, None, "range_invalid");
let mut p = pr(&o6, 3_000, 4_500);
p.bytes = pr(&obj("o6_twin"), 3_000, 4_500).bytes;
add("bytes_of_another_object", p.to_bytes(), h6, None, "range_proof_mismatch");
let mut longer = o7.clone();
longer.extend_from_slice(&object("o7_extension", 1_000));
let mut p = pr(&longer, 20_000, 21_000);
p.content_hash = h7;
add("past_the_true_end_under_a_longer_length", p.to_bytes(), h7, None, "range_proof_mismatch");
let mut p = pr(&o7, 3_000, 7_000);
p.cvs.pop();
add("chaining_value_dropped", p.to_bytes(), h7, None, "range_proof_mismatch");
let mut p = pr(&o7, 3_000, 7_000);
p.cvs.push(p.cvs[0]);
add("chaining_value_added", p.to_bytes(), h7, None, "range_proof_mismatch");
let mut p = pr(&o7, 3_000, 7_000);
p.cvs[1][0] ^= 1;
add("chaining_value_changed", p.to_bytes(), h7, None, "range_proof_mismatch");
add("another_content_hash", pr(&o6, 3_000, 4_500).to_bytes(), h7, None, "range_proof_mismatch");
let mut p = pr(&o7, 3_000, 7_000);
p.content_hash = h6;
add("names_another_content_hash", p.to_bytes(), h7, None, "range_proof_mismatch");
add("audit_outside_the_item", pr(&o7, 10_000, 12_000).to_bytes(), h7, Some(part("o7", 0, 8_192)), "range_not_acknowledged");
let b = pr(&o7, 3_000, 7_000).to_bytes();
add("truncated", b[..b.len() - 1].to_vec(), h7, None, "truncated");
let mut t = b.clone();
t.push(0);
add("trailing_byte", t, h7, None, "malformed");
let mut t = b.clone();
t[4] = 2;
add("version", t, h7, None, "unknown_version");
add("fields_cut_short", b[..40].to_vec(), h7, None, "truncated");
let mut t = b.clone();
t[0] = b'X';
add("magic", t, h7, None, "malformed");
v
}
// ---- the file ----
fn rooms_json(g: &Grants) -> Value {
let d = descriptor();
let mut forged = g.records.clone();
forged.push(forged_grant());
let one = |grants: &[Vec<u8>]| json!({"id": hex(&d.room_id()), "descriptor": hex(&d.to_bytes()), "grants": grants.iter().map(|x| hex(x)).collect::<Vec<_>>()});
json!({"lesson": one(&g.records), "lesson_forged": one(&forged)})
}
fn item_json(i: &Item) -> Value {
json!({"content_hash": hex(&i.content_hash), "start": i.start, "end": i.end})
}
/// The inputs a vector gives: the pool, less what it withholds, with what it adds, and its
/// rooms.
fn inputs_of(doc: &Value, v: &Value) -> Inputs {
let unhex = |s: &Value| unhex(s.as_str().expect("hex")).expect("hex");
let withhold: Vec<[u8; 32]> = v["withhold"].as_array().expect("withhold").iter().map(|x| unhex(x).try_into().expect("32 bytes")).collect();
let mut i = Inputs::default();
let records = doc["records"].as_array().expect("records").iter().chain(v["extra"]["records"].as_array().expect("extra records"));
for r in records {
let b = unhex(&r["record"]);
let d = given_digest(&b);
if withhold.contains(&d) && r.get("name").is_some() {
continue;
}
i.items.insert(d, r["items"].as_array().expect("items").iter().map(unhex).collect());
i.records.insert(d, b);
}
for p in doc["proofs"].as_array().expect("proofs").iter().map(|p| &p["proof"]).chain(v["extra"]["proofs"].as_array().expect("extra proofs")) {
let b = unhex(p);
if !withhold.contains(&proof_digest(&b)) {
i.proofs.insert(proof_digest(&b), b);
}
}
for h in doc["hops"].as_array().expect("hops") {
let b = unhex(&h["hop"]);
if !withhold.contains(&given_digest(&b)) {
i.hops.insert(given_digest(&b), b);
}
}
for name in v["rooms"].as_array().expect("rooms") {
let r = &doc["rooms"][name.as_str().expect("a name")];
i.rooms.push(RoomInput { descriptor: unhex(&r["descriptor"]), grants: r["grants"].as_array().expect("grants").iter().map(unhex).collect() });
}
i
}
fn range_outcome(v: &Value) -> Value {
let b = unhex(v["proof"].as_str().expect("hex")).expect("hex");
let h: [u8; 32] = unhex(v["content_hash"].as_str().expect("hex")).expect("hex").try_into().expect("32 bytes");
let audit = (!v["audit"].is_null()).then(|| Item {
content_hash: unhex(v["audit"]["content_hash"].as_str().expect("hex")).expect("hex").try_into().expect("32 bytes"),
start: v["audit"]["start"].as_u64().expect("start"),
end: v["audit"]["end"].as_u64().expect("end"),
});
range_json(&check_range(&b, &h, audit.as_ref()))
}
/// The corpus file, from fixed seeds.
pub fn generate() -> String {
let w = world();
let mut principals = Map::new();
for (n, i) in PRINCIPALS {
principals.insert(n.into(), Value::String(hex(&pk(i))));
}
let mut objects = Map::new();
for (n, len) in OBJECTS {
objects.insert(n.into(), json!({"content_hash": hex(&content_hash(&obj(n))), "length": len}));
}
let grant_names = json!({"viewer_a": hex(&w.g.ga), "viewer_b": hex(&w.g.gb), "viewer_c": hex(&w.g.gc), "viewer_d": hex(&w.g.gd), "viewer_a_expiring": hex(&w.g.gx)});
let records: Vec<Value> = w
.recs
.values()
.map(|r| json!({"name": r.name, "digest": hex(&r.digest()), "record": hex(&r.bytes()), "items": r.items.iter().map(|x| hex(x)).collect::<Vec<_>>()}))
.collect();
let p = &w.p;
let proofs: Vec<Value> = [("viewer_a", &p.pa), ("viewer_b", &p.pb), ("wrong_key", &p.wrong_key), ("bad_signature", &p.bad_sig), ("wrong_grant", &p.wrong_grant), ("malformed", &p.malformed)]
.iter()
.map(|(n, b)| json!({"name": n, "digest": hex(&proof_digest(b)), "proof": hex(b)}))
.collect();
let hops: Vec<Value> = w.hops.iter().map(|(n, h)| json!({"name": n, "digest": hex(&h.digest()), "hop": hex(&h.to_bytes())})).collect();
let mut doc = json!({
"corpus": CAPABILITY,
"extension": "extensions/settlement.md",
"period": {"start": P_START, "end": P_END},
"tariff_usd": {"currency": "USD", "prices": usd().prices},
"principals": principals,
"objects": objects,
"rooms": rooms_json(&w.g),
"grants": grant_names,
"records": records,
"proofs": proofs,
"hops": hops,
"statements": [],
"ranges": [],
});
let mut statements = Vec::new();
for v in vectors(&w) {
let rooms = v.given.rooms.clone().unwrap_or_else(|| vec!["lesson"]);
let mut j = json!({
"name": v.name,
"statement": hex(&v.statement),
"rooms": rooms,
"withhold": v.given.withhold.iter().map(|d| hex(d)).collect::<Vec<_>>(),
"extra": {
"records": v.given.extra_records.iter().map(|(b, items)| json!({"record": hex(b), "items": items.iter().map(|x| hex(x)).collect::<Vec<_>>()})).collect::<Vec<_>>(),
"proofs": v.given.extra_proofs.iter().map(|b| hex(b)).collect::<Vec<_>>(),
},
});
let got = outcome_json(&super::verify(&v.statement, &inputs_of(&doc, &j)));
let reached = if got["outcome"] == "accept" { "accept" } else { got["reason"].as_str().expect("a reason") };
assert_eq!(reached, v.expect, "statement vector {}: {got}", v.name);
j["outcome"] = got;
statements.push(j);
}
let mut ranges = Vec::new();
for v in range_vectors() {
let mut j = json!({
"name": v.name,
"proof": hex(&v.proof),
"content_hash": hex(&v.hash),
"audit": v.audit.as_ref().map_or(Value::Null, item_json),
});
let got = range_outcome(&j);
let reached = if got["outcome"] == "accept" { "accept" } else { got["reason"].as_str().expect("a reason") };
assert_eq!(reached, v.expect, "range vector {}: {got}", v.name);
j["outcome"] = got;
ranges.push(j);
}
doc["statements"] = Value::Array(statements);
doc["ranges"] = Value::Array(ranges);
let mut s = serde_json::to_string_pretty(&doc).expect("JSON");
s.push('\n');
s
}
/// Every vector of `doc` against this module: the failures.
pub fn verify(doc: &str) -> Vec<String> {
let doc: Value = match serde_json::from_str(doc) {
Ok(d) => d,
Err(e) => return vec![format!("vectors.json does not parse: {e}")],
};
let mut fails = Vec::new();
for v in doc["statements"].as_array().into_iter().flatten() {
let got = outcome_json(&super::verify(&unhex(v["statement"].as_str().unwrap_or("")).unwrap_or_default(), &inputs_of(&doc, v)));
if got != v["outcome"] {
fails.push(format!("statement {}: stated {}, got {got}", v["name"], v["outcome"]));
}
}
for v in doc["ranges"].as_array().into_iter().flatten() {
let got = range_outcome(v);
if got != v["outcome"] {
fails.push(format!("range {}: stated {}, got {got}", v["name"], v["outcome"]));
}
}
fails
}

View file

@ -224,7 +224,7 @@ fn every_decoded_output_states_a_sha256() {
("ops-conformance", "integer operators: every operator, stage and graph vector, the lower_* decodes of the sets under sets/ among them, states its output integers in full or its refusal code, never a digest of them; certificates/ states each lowered set's SHA-256 (a parameter-set identity) and holds the lowered graphs as bytes, compared byte for byte; the output digests a capability decoded through its lowering reproduces (integer-operators §14.4) are stated by that capability's own corpus"),
("signer-conformance", "key logs, grants and statuses: signed documents, no decoded output"),
("edge-conformance", "derivation records of a deriving edge: each replicate-tier output is recomputed and compared byte for byte by its verifier, and every output is named by its content hash in the record"),
("live-conformance", "live events, rooms, takes, compositions, recordings, captions, metering, steering, moderation, watermark sessions, ads, AI usage and contribution records: signed records, commitments, packages, clips, cues and their renderings, per-hop records, ledgers and session receipts with their units and joules recomputed, steering policies and decisions with each pathway's evidence and the steering manifests recomputed byte for byte, review policies, decisions, notices and the items' timelines with each statement-of-reasons export written byte for byte, watermark tokens, session records, recovered sequences and disclosures, ad policies, breaks with their interstitial tags, events and asset lists written byte for byte and their impression receipts, usage statements with every term's preference, receipted envelopes (whose payloads are opaque test bytes no case decodes), room usage records, HTTP fields, catalogs, consumption claims and gate decisions, and their verdicts, and layouts and mixes stated in full, rectangle for rectangle and sample for sample; nothing decoded against a parameter set but the contribution corpus's generated pictures, which wai.generate.spectral makes with its model file and which the corpus names, every one, by the SHA-256 of its RGB8 samples"),
("live-conformance", "live events, rooms, takes, compositions, recordings, captions, metering, steering, moderation, watermark sessions, ads, AI usage, contribution records and settlement: signed records, commitments, packages, clips, cues and their renderings, per-hop records, ledgers and session receipts with their units and joules recomputed, steering policies and decisions with each pathway's evidence and the steering manifests recomputed byte for byte, review policies, decisions, notices and the items' timelines with each statement-of-reasons export written byte for byte, watermark tokens, session records, recovered sequences and disclosures, ad policies, breaks with their interstitial tags, events and asset lists written byte for byte and their impression receipts, usage statements with every term's preference, receipted envelopes (whose payloads are opaque test bytes no case decodes), room usage records, HTTP fields, catalogs, consumption claims and gate decisions, acknowledgements of delivery with their item lists, range proofs over objects' BLAKE3 chunk trees (whose objects are opaque test bytes no case decodes) and settlement statements with every line, amount and per-class joule total recomputed, and their verdicts, and layouts and mixes stated in full, rectangle for rectangle and sample for sample; nothing decoded against a parameter set but the contribution corpus's generated pictures, which wai.generate.spectral makes with its model file and which the corpus names, every one, by the SHA-256 of its RGB8 samples"),
("http-conformance", "objects and head proofs for transport: no case decodes a payload, and the component a fetch case picks is stated by its SHA-256"),
("cache-conformance", "cache rules: a driver, its controls and their reports, with no objects of its own"),
("byte-exact-conformance", "goldens over tests/vectors, whose sidecars state the SHA-256"),

View file

@ -0,0 +1,84 @@
//! `wai.delivery.settlement` (extensions/settlement.md): the committed corpus
//! `live-conformance/settlement/vectors.json` regenerates byte for byte from its fixed seeds and
//! is the only file there besides its README, every statement and range proof in it gets the
//! outcome it states, every reason the extension adds refuses a vector, the accepted vectors are
//! the cases §12 names, and the extension's §11 lists exactly the reasons the reference reports.
#![cfg(feature = "settlement")]
use std::collections::BTreeSet;
use std::path::PathBuf;
use wai::settlement::{corpus, Reason};
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn read(p: &str) -> String {
std::fs::read_to_string(wai_dir().join(p)).unwrap_or_else(|e| panic!("{p}: {e}"))
}
fn committed() -> String {
read("live-conformance/settlement/vectors.json")
}
#[test]
fn the_committed_corpus_regenerates_from_its_seeds() {
assert!(committed() == corpus::generate(), "live-conformance/settlement/vectors.json differs from `wai_settlement_vectors gen`");
let mut files: Vec<String> = std::fs::read_dir(wai_dir().join("live-conformance/settlement")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
files.sort();
assert_eq!(files, ["README.md", "vectors.json"]);
}
#[test]
fn every_statement_and_range_proof_gets_its_stated_outcome() {
let fails = corpus::verify(&committed());
assert!(fails.is_empty(), "{}", fails.join("\n"));
}
/// Every reason this extension adds, and every code of another extension it reuses, refuses a
/// vector; the accepted statements are §12's, with the figures each was written for.
#[test]
fn the_corpus_covers_every_reason_and_case() {
let doc: serde_json::Value = serde_json::from_str(&committed()).unwrap();
let mut seen: BTreeSet<&str> = BTreeSet::new();
for section in ["statements", "ranges"] {
seen.extend(doc[section].as_array().unwrap().iter().filter_map(|v| v["outcome"]["reason"].as_str()));
}
for r in Reason::ALL {
assert!(seen.contains(r.code()), "nothing in the corpus is refused {r}");
}
for r in Reason::REUSED {
assert!(seen.contains(r), "nothing in the corpus is refused {r}");
}
let get = |n: &str| doc["statements"].as_array().unwrap().iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no vector {n}"))["outcome"].clone();
for n in ["relay_paid_by_edge_and_viewer", "edge_fetch_granted_and_free", "origin_fills_billed_to_grant", "viewer_forwards_sealed_objects", "relay_unmetered_hop"] {
assert_eq!(get(n)["outcome"], "accept", "{n}");
}
let relay = get("relay_paid_by_edge_and_viewer");
assert_eq!(relay["coverage"], "whole");
assert_eq!(relay["totals"].as_array().unwrap().len(), 2, "one total per class, never one across them");
assert_eq!(relay["unacknowledged"]["declarations"], 2);
assert!(relay["unacknowledged"]["bytes"].as_u64().unwrap() > 0, "declared delivery is never zero");
let edge = get("edge_fetch_granted_and_free");
let lines = edge["lines"].as_array().unwrap();
assert!(lines.iter().any(|l| l["class"] == "fetch" && l["free"] == true && l["amount"] == 0), "a fetch served free");
assert!(lines.iter().any(|l| l["class"] == "fetch" && l["free"] == false && l["amount"].as_u64().unwrap() > 0), "a fetch billed to a grant");
assert_eq!(get("viewer_forwards_sealed_objects")["currency"], serde_json::Value::Null);
assert_eq!(get("relay_unmetered_hop")["coverage"], "partial");
let range = |n: &str| doc["ranges"].as_array().unwrap().iter().find(|v| v["name"] == n).unwrap_or_else(|| panic!("no range {n}"))["outcome"].clone();
assert_eq!(range("tail_establishes_length")["length_established"], true);
assert_eq!(range("interior")["length_established"], false);
assert_eq!(range("length_not_established")["outcome"], "accept");
assert_eq!(range("length_not_established")["length_established"], false);
}
/// §11 of the extension lists exactly the codes `Reason::ALL` has, each once, in order.
#[test]
fn the_extension_lists_the_reasons() {
let text = read("extensions/settlement.md");
let at = text.find("\n## 11. Reasons").expect("a §11");
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
let listed: Vec<String> = text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
let want: Vec<String> = Reason::ALL.iter().map(|r| r.code().to_string()).collect();
assert_eq!(listed, want, "§11's table and Reason::ALL differ");
}