open-standards/wai/extensions/fleet.md
Transaction Science e39297fac6 wai.sink.models: which models a fleet's sinks may run, and what they ran
A sink is a computer that runs named, pinned models: codec priors, DRED's weights, an exact
upscale's prior, enhancement and generator models. Nothing said which of them a fleet's sinks may
run, revoked one across a fleet, or rolled up what each class of device ran.

extensions/model-governance.md registers the record `wai.sink.models`, in the `models` profile
(core/profiles.json, CORE.md's table), outside the core. An operator signs a model policy
(`wai-model-policy`): classes of devices, enrolled by key or vouched for by an attestation key
(`wai-model-device`), and for each class the parameter sets it may run by the digest a pin names
(SPEC §3.1), per capability and role (decode, upscale, enhance, generate), with the least tier each
must run at; a span, a successor that takes its place from its own start, and the operator's key
log (jwp-receipts §7), whose `revoked_modules` revoke a set from the first revision that lists it:
the list SPEC §3.1 check 5 already reads at a sink. A device signs a run report (`wai-model-run`)
from what it already states: each decode's neural-dispatch report with the set it read (a sealed
set's opened digest too), and its sessions' exact-upscale presentation statements, whose sink-made
pictures name their prior or model. A verifier refuses a set outside the policy
(`model_not_in_policy`), a revoked set run at or after its revocation (`model_revoked`), a period
outside the policy's time in force (`policy_expired`, `policy_not_yet_valid`), a run below the
policy's tier (`model_tier_below_policy`) and a float path claiming decode-equivalence
(neural-dispatch's `dispatch_tier_refused`). The operator's roll-up (`wai-model-rollup`) states per
class the reports, the devices that reported and those enrolled that did not, and each set's
decodes by unit, frames and pixels, recomputed from the reports (`rollup_mismatch`).
neural-dispatch §5, exact-upscale §6.2 and fleet §1 point to it; no record they define changes.

The reference is the `model_policy` module of wai-rs (feature `model_policy`, in `full`);
`wai model verify` (catalogue `model.verify`) verifies a bundle (`*.wai-models.json`), and `wai
verify` reads a bundle and each record alone. The corpus is live-conformance/models/ (57 bundles:
3 accepted, 54 refused, every reason of §10 and every reused reason at least once), and
model_policy_verify.py is its second implementation, written from the text: it agrees on every
outcome, reason, place and statement, 14 of 14 mutated copies fail, and 51 of 51 weakened
verifiers are caught. vectors.json has one SHA-256 when generated on x86_64 and on arm64. 22
faults planted in the reference (a capability or role ignored, the revocation instant excluded, a
sealed set's inner identity ignored, the successor's start ignored, expiry unchecked, a float
path's claimed tier believed, silent devices and units miscounted, the signing domain, a period's
end included, the attestation's span, a statement counted twice, overlap, the roll-up's signer
and reports, a path's arithmetic, pictures' revocation, the tier compared the wrong way, the
successor chain, the key log ignored, an unnamed model admitted) each fail its tests. The
registry export, the operation catalogue and Studio's actions are regenerated by their tools.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 14:23:40 -05:00

590 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Fleet
> Status: Draft. Registers the record `wai.delivery.fleet`: the nodes an organisation runs,
> enrolled by their signing keys, and a report of what they did that anyone holding their
> records recomputes. The organisation signs an **enrolment**, `WFE1`, for each node: the
> node's Ed25519 key, its role (a relay, a compositor, an ingest gateway, a room service, a
> ledger or an audit collector), a label and the span the enrolment holds. A node contributes
> the records it already signs, as it signs them: a relay's session summaries, and every hop's
> per-hop session records ([live-metering](live-metering.md) §3) with the room descriptors that
> place them in time. For a period, the organisation signs a **fleet report**, `WFR1`: one line
> per node and one per room, with the sessions, bytes and energy their records state, each line
> citing the records it came from, and the fleet's totals. Energy keeps its acquisition class
> and its declared uncertainty, one row per class and per scope, never summed across either.
> Every unmetered figure is counted as unmetered, never as zero joules, and a total over
> measured and unmetered figures states that it is partial. A verifier given a report with its
> enrolments and records, a **bundle**, recomputes every line and refuses a report that does not
> recompute. No accounts, no billing and no new transport. The reference implementation is the
> `fleet` module of `wai-rs` (feature `fleet`) and the service `wai/wai-fleet`; the corpus is
> [`live-conformance/fleet/`](../live-conformance/fleet/), and its second implementation is
> `live-conformance/fleet_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision: the WAI texts this one cites; RFC 3629; RFC 8032; RFC 8259;
> FIPS 180-4.
## 1. Scope
In scope: the organisation key and the enrolment of a node (§3); the records a node contributes,
how each is read, verified and placed in time, and how one thing is counted once (§4); the
figures a report states for each node, each room and the fleet, with their energy and citations
(§5); the report, its signature and its rules (§6); the bundle and its verification (§7); how the
fleet composes with the [receipt ledger](receipt-ledger.md) (§8); and the reference service and
its operations (§9).
Out of scope: accounts, members, single sign-on, quotas, billing and any control plane shared by
several organisations; a transport; how a node measures its energy, which
[energy-measurement](energy-measurement.md) governs; the records a node signs, which their own
texts and implementations define and this one reads unchanged; and prices, which the receipt
ledger states.
Relationship to the core spec: `wai.delivery.fleet` 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.
The sinks a fleet's nodes serve are not enrolled here. An operator enrols its sinks, by their own
keys and in classes, in a model policy ([model-governance](model-governance.md)), which says which
parameter sets each class may run; the two share no record.
## 2. Terms
- **Organisation**: the party that runs a set of nodes. Its **organisation key**, an Ed25519 key
(RFC 8032), signs its enrolments and its reports.
- **Node**: a process the organisation runs that signs records, named by its Ed25519 key, the
**node key**. A relay's node key is the key it signs its session summaries with, which is also
the key its per-hop records name as `hop` (live-metering §3.1) and its relay id among relays
(live-rooms §10.7); a compositor's or an ingest gateway's is the key its per-hop records name.
- **Role**: what a node is, by code: 1 relay, 2 compositor, 3 ingest gateway (live-metering §2's
roles, with the codes a per-hop record gives them), 4 room service, 5 ledger, 6 audit collector.
- **Time**: microseconds since 1970-01-01T00:00:00Z. A **period** is a span of time from its
`start` (included) to its `end` (excluded).
- **Record**: one signed thing a bundle holds (§4.1), with its **kind** (a u8) and its
**identity** (32 bytes). Its **signer** is the key that signed it.
- *u64*, *u32*, *u16*, *u8*: little-endian unsigned integers of that many bits. A **digest** is
SHA-256 (FIPS 180-4), 32 bytes. Hexadecimal is lowercase.
- **Class**, **tag**, **unmetered marker**: an acquisition class of energy-measurement §2.7, its
tag byte (2 `ModelBased`, 3 `Estimator`, 4 `OnChipCounter`, 5 `CalibratedInstrument`), and a
figure of 0 with no class (energy-measurement §2), which is never a figure of zero joules.
- **Scope** of an energy figure: 1 `sessions`, the figure a session summary states (§4.2); 2
`hops`, the figure a per-hop record states (§4.3).
## 3. Enrolment
### 3.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WFE1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `organisation` | 32 | the organisation key, which signs the enrolment |
| `node` | 32 | the node key |
| `role` | 1 | the node's role (§2) |
| `from`, `until` | 8 each | u64: the span the enrolment holds, from `from` (included) to `until` (excluded); `until` is 2^64 − 1 for a span with no end |
| `label` | 1 + *n* | a u8 *n*, then *n* bytes of UTF-8 (RFC 3629): the node's name in reports, empty for none |
| `sig` | 64 | §3.2 |
### 3.2 Digest and signature
The **digest** of an enrolment is SHA-256 over its bytes before `sig`. Its signature is Ed25519
by `organisation` over the 16 bytes `wai:fleet-enrol` (ASCII) and 0x01, followed by the bytes the
digest covers, 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.3 Reading and rules
A reader reads exactly the bytes it is given as one enrolment, as live-events §3.3 reads a
record, with `WFE1` for `WEV1` (`truncated`, `malformed`, `unknown_version`). It then refuses as
`enrolment_invalid` an enrolment that breaks any of these:
- `role` is 1 to 6;
- `from` is below `until`;
- `label` is at most 64 bytes of UTF-8 and holds no control character (U+0000 to U+001F and
U+007F);
- `node` is not `organisation`.
Then the signature (§3.2).
### 3.4 What each role contributes
| role | contributes in this revision |
|---|---|
| 1 relay | its session summaries (§4.2) and its per-hop records of role 1 (§4.3) |
| 2 compositor | its per-hop records of role 2 |
| 3 ingest gateway | its per-hop records of role 3 |
| 4 room service, 5 ledger, 6 audit collector | none: the node is enrolled and listed, and reported silent |
A record of a kind its signer's role does not contribute, and a per-hop record whose `role` is not
its signer's, is refused `role_mismatch`. A node holds one enrolment in a fleet: a second enrolment
of the same node key is refused `enrolment_duplicate`.
## 4. Records
### 4.1 Kinds and their carriage
A record is carried as a JSON object (RFC 8259) whose `kind` names its kind, with exactly the
members of its kind:
| `kind` | members | record kind | identity | signer |
|---|---|---|---|---|
| `hop` | `record` and `ledger`, hexadecimal: a per-hop record and its ledger (live-metering §3) | 2 | its digest (live-metering §3.5) | its `hop` |
| `summary` | `line`, a string: one line of a relay's summary file, without its line end (§4.2) | 10 | SHA-256 over the summary's text, *S* | its `relay_key` |
| `descriptor` | `descriptor`, hexadecimal: a room descriptor (live-rooms §3) | 11 | its digest, which is the room id (live-rooms §3.2) | none: its digest binds it (§4.4) |
Kind 2 is the kind and identity [receipt-ledger](receipt-ledger.md) §5.2 gives a per-hop record,
so a fleet report and an invoice name such a record alike (§8); kinds 10 and 11 are kinds that
table does not use. A carriage that is not such an object, whose `kind` is none of these, or
whose members are not exactly its kind's, each of its type, is refused `malformed`.
### 4.2 The session summary
The reference relay keeps a summary of every MOQT session it serves, signs it when the session
ends, and appends it to its summary file, one line each (live-metering §8). Its format is named
`joule-moq-relay/session-summary/2`.
**The line.** A line is exactly the 11 bytes `{"summary":`, the summary's text *S*, the 14 bytes
`,"signature":"`, 128 hexadecimal digits (the signature, 64 bytes), and the 2 bytes `"}`. That is
the form the reference relay writes; a line of any other form is `malformed`.
**The text.** *S* is read as JSON text (RFC 8259) of one object: valid UTF-8, no member named twice
in one object, no value inside more than 63 objects and arrays, no escaped surrogate that is not
one of a pair, and nothing after the value; otherwise it is `malformed`. An **integer** here is a
number spelled as a plain decimal integer, with no sign, fraction or exponent and no leading zero,
below 2^64. These members are read, in the order of the table, and the first that is missing or of
another type is `malformed`:
| member | type | meaning |
|---|---|---|
| `format` | string | `joule-moq-relay/session-summary/2`; any other string is `unknown_version` |
| `relay_key` | string of 64 hexadecimal digits | the relay's node key, the signer |
| `session` | integer | the relay's number for the session |
| `opened_unix_ms`, `closed_unix_ms` | integer | when the session opened and closed, in milliseconds since 1970-01-01T00:00:00Z |
| `relay_energy` | object | the session's own energy figure: `acquisition` (string) and `joules_micro` (integer) |
| `tally` | object | what the session carried |
| `tally.peer` | string of 64 hexadecimal digits, or absent | the relay at the other end, when the session is a hop link (live-metering §3.10) |
| `tally.publisher` | object | holding `object_bytes_received` (integer): the payload bytes the session published |
| `tally.subscriber` | object | holding `object_bytes_forwarded` (integer), the payload bytes forwarded to the session live, and `tracks`, an object whose every member is an object holding, or not, `fetch_bytes` (integer; 0 when absent): the payload bytes it was served by fetch and fill on that track |
Every other member is signed and is not read: the reference relay adds members as it counts more,
and the signature covers them. Of those, `tally.publisher_signed` states the figures publishers
signed for producing the objects the session received, which are the publishers' energy and not
the relay's: a report states none of them.
A summary that reads is then checked in this order: its energy, its rules, its signature.
**Energy.** `relay_energy` holds exactly its two members. With `acquisition` `unmetered` and
`joules_micro` 0, the figure is the unmetered marker; with `ModelBased` or `Estimator` and
`joules_micro` above 0, it is a figure of tag 2 or 3. Anything else is refused `energy_invalid`:
another label, a figure beside `unmetered`, a class beside zero, and the classes whose evidence
(energy-measurement §2.7) a summary has no member to carry. The reference relay states the marker
always: sessions run together in one process, and its counter cannot tell their shares apart
(live-metering §8).
**Rules.** A summary is refused `record_invalid` when `opened_unix_ms` is above `closed_unix_ms`,
when `closed_unix_ms` × 1000 is 2^64 or more, or when the bytes out it states, its
`object_bytes_forwarded` and the sum of its tracks' `fetch_bytes`, add to 2^64 or more.
**Signature and identity.** The signature is Ed25519 by `relay_key` over the 32 bytes
`joule-moq-relay:session-summary` (ASCII) and 0x01, followed by *S*, verified strictly (SPEC §7
*Signatures*: `bad_key`, `bad_signature`). The identity is SHA-256 over *S*.
### 4.3 The per-hop record
A per-hop record is read as live-metering §3.6 reads one, of the room its own `room` field names,
with that section's codes (`truncated`, `malformed`, `unknown_version`, `energy_invalid`,
`record_invalid`), and its signature is verified as §3.5 there requires (`bad_key`,
`bad_signature`). Its ledger is read as §3.6 there reads a ledger (`ledger_invalid`), and has
`grants` entries and the root `ledger`: otherwise `ledger_mismatch`. The size bound of
live-metering §3.7 is not applied: it depends on the media tracks the room published, which a fleet
does not hold, and the reference service bounds what one request may carry instead (§9).
Its figure is its `joules` with its class, in scope 2: the hop's own energy over its span, which
the reference relay states as `OnChipCounter` with its evidence where its counter was live across
the span and it served that room alone, and as the unmetered marker otherwise (live-metering §8).
### 4.4 The room descriptor
A per-hop record states its span on its room's clock. The fleet reads, of the room's descriptor
(live-rooms §3.1), only what places that span in time: `magic`, which is `WRD1` (fewer than 4
bytes: `truncated`; another: `malformed`); `version`, which is 1 (`unknown_version`); a length of
at least 141 bytes (`truncated`); `authority`, the 32 bytes after the version; and `t0`, the u64 at
byte 69. Its signature is verified as live-rooms §3.2 requires: Ed25519 by `authority` over the 14
bytes `wai:live-room` (ASCII) and 0x01, followed by every byte but the last 64, strictly (`bad_key`,
`bad_signature`). Its digest is SHA-256 over every byte but the last 64, and is the room id.
A descriptor needs no enrolment. Its digest binds it to the room id a per-hop record names, and the
record's own signature binds the room id to its hop, so the `t0` that places a record is the `t0`
of the room its hop signed for. The descriptor's other fields are the room verifier's to read
(live-rooms §9).
### 4.5 Placement in time
Each summary and each per-hop record has an **instant**:
- a summary's is `closed_unix_ms` × 1000: a session is reported in the period in which it ended;
- a per-hop record's is `t0` + `end` − 1, the last microsecond its span covers, with the `t0` of
the descriptor among the records whose digest is its `room`. A per-hop record whose room has no
such descriptor is refused `descriptor_missing`, and one whose `t0` + `end` − 1 is 2^64 or more
is in no period.
A record is **in** a period when its instant is at or after the period's `start` and before its
`end`; otherwise it is refused `outside_period`. It is **within its enrolment** when its instant
is at or after its signer's `from` and before its `until`: otherwise `outside_enrolment`. So of consecutive periods,
each record is in exactly one.
### 4.6 One thing once
Of two records, the later is refused:
- `record_duplicate` when both have one kind and one identity;
- `record_overlap` when both are summaries of one relay with one `session` and one
`opened_unix_ms` (one session summarised twice), or both are per-hop records of one hop and one
room whose spans share an instant, spans from *s*₁ to *e*₁ and from *s*₂ to *e*₂, each end
excluded, with *s*₁ < *e*₂ and *s*₂ < *e*₁ (live-metering §3.8: a hop's parts of one room session
do not overlap).
## 5. The figures
A report's figures are computed from its records: every summary and every per-hop record, each
in its period and within its enrolment, and the descriptors that place them.
### 5.1 Node lines
There is one **node line** for each node that signed at least one summary or per-hop record, in
ascending order of node key:
| figure | what it is |
|---|---|
| `node`, `role` | the node key, and its enrolment's role |
| `sessions` | its summaries with no `tally.peer`: the sessions it served for clients |
| `links` | its summaries with a `tally.peer`: its hop links to other relays |
| `bytes_in`, `bytes_out` | over its summaries with no `tally.peer`: the sum of `object_bytes_received`; the sum of `object_bytes_forwarded` and the tracks' `fetch_bytes` |
| `link_bytes_in`, `link_bytes_out` | the same, over its summaries with a `tally.peer` |
| `hops` | its per-hop records (u32) |
| `hop_bytes_in`, `hop_bytes_out` | over its per-hop records, the sum of `bytes` of their track entries in (`dir` 1) and out (`dir` 2) |
| energy | its summaries' figures in scope 1, its per-hop records' in scope 2 (§5.4) |
| citations | its summaries and per-hop records (§5.5) |
The bytes of the summaries and the bytes of the per-hop records are two accounts of a relay's
traffic and are never added: a relay's summaries count every object of every session, and its
per-hop records count the objects of each room's tracks, a hop link's at both of its ends
(live-metering §3.10). A compositor or an ingest gateway signs no summaries, and its traffic is in
its per-hop records alone.
### 5.2 Room lines
There is one **room line** for each room a per-hop record names, in ascending order of room id:
| figure | what it is |
|---|---|
| `room`, `t0` | the room id, and its descriptor's `t0` |
| `hops` | its per-hop records (u32) |
| `sessions` | the sum of their `sessions`: subscriber sessions, each counted at the hop that served it (live-metering §3.3) |
| `subscriber_us` | the sum of their `subscriber_us` over the nine layer classes |
| `bytes_in`, `bytes_out` | the sum of `bytes` of their track entries in and out |
| energy | their figures, in scope 2 |
| citations | its per-hop records and its descriptor |
A room's bytes are the bytes its hops handled: an object carried over a hop link is counted out at
one relay and in at the next.
### 5.3 Totals
The **totals** are the sums over the node lines of `sessions`, `links`, `bytes_in`, `bytes_out`,
`link_bytes_in`, `link_bytes_out`, `hops`, `hop_bytes_in` and `hop_bytes_out`; and their energy is
every summary's figure in scope 1 and every per-hop record's in scope 2.
### 5.4 Energy
Each summary states one figure, in scope 1, and each per-hop record one, in scope 2. Over the
records of a line, or of the totals, for each scope:
- **`unmetered`** counts the figures that are the unmetered marker. They are in no row, and a
reader **MUST NOT** present them as zero joules.
- There is one **row** for each tag among the metered figures: `figures`, their number; `joules`,
their sum in microjoules; and `max_ppm`, the largest `relative_ppm` among them for tags 4 and 5,
which carry a declared uncertainty, and 0 for tags 2 and 3, which do not. When each figure's
error is within its declared relative uncertainty, the sum's is within `max_ppm` of the sum.
A row never sums figures of two classes, and no row sums two scopes, as receipt-ledger §6.6
requires of an invoice: a counter reading and a model's output added are neither. A scope's
**coverage** is `whole` when it has a row and no unmetered figure, **`partial`** when it has both,
`unmetered` when it has unmetered figures alone, and `none` when it has no figure. The rows of a
partial scope are the energy of its metered records alone, beside a count of records whose energy
nobody measured, and **MUST NOT** be presented as the energy of the line or of the fleet. A report
whose `unmetered` for a scope is below the unmetered markers of its records has counted a figure
nobody measured as something else, and is refused `unmetered_summed`.
A sum that a u64 cannot hold cannot be stated, and an organisation does not sign a report that
would state one.
### 5.5 Citations
Each line cites the records it came from (§5.1, §5.2): `cited` is their number, and `cites` is
SHA-256 over the 16 bytes `wai:fleet-cites` (ASCII) and 0x01, followed by each cited record's kind
(u8) and identity (32 bytes), in ascending order of (kind, identity). Every cited record is in the
report's `records`, and a line that cites none is refused `line_unsourced`. A reader shows one line
to an auditor with the records it cites and nothing else, and the auditor recomputes it.
## 6. The report
### 6.1 Layout
| field | size | meaning |
|---|---|---|
| `magic` | 4 | `WFR1` (ASCII) |
| `version` | 1 | 1 in this revision |
| `organisation` | 32 | the organisation key, which signs the report |
| `start`, `end` | 8 each | u64: the period it reports |
| `records` | 4 + 33 × *r* | a u32 count, then each record: `kind` (u8) and `identity` (32), in strictly ascending order of (`kind`, `identity`) |
| `enrolments` | 2 + 64 × *k* | a u16 count, then each enrolment: `node` (32) and its digest (32), in strictly ascending order of `node` |
| `nodes` | 2 + … | a u16 count, then each node line (§6.2), in strictly ascending order of `node` |
| `rooms` | 2 + … | a u16 count, then each room line (§6.2), in strictly ascending order of `room` |
| `totals` | 68 + … | §6.2 |
| `sig` | 64 | §6.3 |
### 6.2 Lines, totals and the energy block
A **node line** is `node` (32), `role` (u8), `sessions`, `links`, `bytes_in`, `bytes_out`,
`link_bytes_in` and `link_bytes_out` (u64 each), `hops` (u32), `hop_bytes_in` and `hop_bytes_out`
(u64 each), an energy block, `cited` (u32) and `cites` (32).
A **room line** is `room` (32), `t0` (u64), `hops` (u32), `sessions`, `subscriber_us`, `bytes_in`
and `bytes_out` (u64 each), an energy block, `cited` (u32) and `cites` (32).
The **totals** are `sessions`, `links`, `bytes_in`, `bytes_out`, `link_bytes_in` and
`link_bytes_out` (u64 each), `hops` (u32), `hop_bytes_in` and `hop_bytes_out` (u64 each), and an
energy block.
An **energy block** is a u8 count *e*, then *e* rows, each `scope` (u8), `tag` (u8), `figures`
(u32), `joules` (u64, µJ) and `max_ppm` (u32), in strictly ascending order of (`scope`, `tag`);
then `unmetered`, two u32: the unmetered figures of scope 1 and of scope 2.
### 6.3 Digest and signature
The **digest** of a report is SHA-256 over its bytes before `sig`. Its signature is Ed25519 by
`organisation` over the 17 bytes `wai:fleet-report` (ASCII) and 0x01, followed by the bytes the
digest covers, verified strictly (SPEC §7 *Signatures*: `bad_key`, `bad_signature`).
### 6.4 Reading and rules
A reader reads exactly the bytes it is given as one report, as §3.3 reads an enrolment with `WFR1`
for `WFE1` (`truncated`, `malformed`, `unknown_version`), and refuses as `classes_summed` a report
with a row of tag 0: a sum over figures of no one class. It refuses as `report_invalid` one that
breaks any of these:
- `start` is below `end`;
- each list is in the strictly ascending order §6.1 gives it, and each record's `kind` is 2, 10
or 11;
- each node line's `role` is 1 to 6;
- each row's `scope` is 1 or 2, its `tag` 2 to 5, its `figures` above 0, and its `max_ppm` 0 when
its tag is 2 or 3;
- a room line's energy block has no row of scope 1, and 0 unmetered figures of scope 1.
Then the signature (§6.3).
## 7. The bundle and its verification
### 7.1 The bundle
A **bundle** is one JSON object with exactly these members: `kind`, the string
`wai-fleet-bundle`; `report`, the report in hexadecimal; `enrolments`, an array of enrolments in
hexadecimal; and `records`, an array of records in their carriage (§4.1). A bundle that is not such
an object is refused `malformed`, at `bundle`. A file that holds one is named with the suffix
`.wai-fleet.json`.
The report's records are every record of the bundle, and its enrolments are every enrolment of the
bundle: a bundle is a report with exactly what it rests on.
### 7.2 Steps
A verifier given a bundle runs these steps and stops at the first refusal, which it reports with
the reason and where:
1. **The report** (§6.4), at `report`: `truncated`, `malformed`, `unknown_version`,
`classes_summed`, `report_invalid`, `bad_key`, `bad_signature`.
2. **Each enrolment**, in order, at `enrolment` and its index: it reads and its signature verifies
(§3.3: `truncated`, `malformed`, `unknown_version`, `enrolment_invalid`, `bad_key`,
`bad_signature`); its `organisation` is the report's (`wrong_organisation`); its `node` is no
earlier enrolment's (`enrolment_duplicate`); and its span meets the report's period, `from`
below the period's `end` and the period's `start` below `until` (`outside_period`).
3. **Each record**, in order, at `record` and its index: it reads and its signature verifies, by its
kind (§4.1 to §4.4, with their codes).
4. **Each summary and per-hop record**, in order, at `record` and its index: its signer is the
`node` of an enrolment (`signer_not_enrolled`); its kind and its `role` are its signer's role's
(§3.4, `role_mismatch`); it is placed (§4.5, `descriptor_missing`); it is in the report's period
(`outside_period`); and it is within its enrolment (`outside_enrolment`).
5. **Across records**, at `record` and the index of the later (§4.6): `record_duplicate`,
`record_overlap`.
6. **The figures**, recomputed from the bundle as §5 computes them, with the report's `records`
and `enrolments` (§7.1), and compared in this order:
1. a node line whose node signed no summary or per-hop record of the bundle, a room line whose
room no per-hop record of the bundle names, or a line whose `cited` is 0:
`line_unsourced`, at `report`, `nodes` or `rooms`, and the line's index;
2. a row of a line or of the totals whose `joules` is not the sum of the figures of its scope
and class, and is the sum of the figures of its scope over its class and at least one other
class of that scope: `classes_summed`, at the line's place, or at `report` and `totals`;
3. a line or the totals whose `unmetered` for a scope is below the unmetered markers of its
records (§5.4): `unmetered_summed`, at the same places;
4. a line whose `cited` or `cites` is not the records it came from: `citation_mismatch`, at the
line's place;
5. any other difference: the records, the enrolments, a line missing or of another subject or
role, a figure, a row, `unmetered`, or the totals: `report_mismatch`, at `report`.
Steps 6.2 to 6.4 compare each line of the report with the recomputed line of the same node or
room where there is one. A figure that cannot be held in its field is a difference.
### 7.3 Outcome
- **refuse**: the reason and where.
- **accept**: the report's digest; the organisation; the period; the number of records; each
enrolment with its node, role, label, span and digest, the number of records it signed, and
whether it is **silent** (it signed none in the period); each node line with its label and
figures; each room line with its figures; the totals; for each line and for the totals, each
scope's rows by class label with their figures, joules and `max_ppm`, its unmetered figures and
its coverage; and each line's cited records, by kind and identity.
A silent node is not an unmetered one. An unmetered node signed records whose energy nobody
measured; a silent node signed no record in the period, and the report states nothing of what it
did.
## 8. With the receipt ledger
The fleet and the receipt ledger read the same records and compute different things from them,
and neither recomputes the other's:
- A per-hop record is named in a report as receipt-ledger §5.2 names it, kind 2 and its digest, so
a room line's citations and an invoice's `records` name the same records alike.
- A per-hop record is carried with its ledger, in the members a ledger's `room` source holds in
`hops` (receipt-ledger §5.1: each `record` and `ledger`). The records a room line cites are given
to a ledger as they are, and the reference service serves them by room (§9).
- A report states no price, tariff or amount, and an invoice no fleet figure.
## 9. The reference service and its operations
`wai/wai-fleet` runs a fleet as an HTTP service over a directory it owns, under the organisation
key:
| request | does |
|---|---|
| `GET /v1/fleet` | the service, its organisation key, its limits |
| `GET /v1/fleet/health` | `ok` |
| `POST /v1/fleet/enrolments` | `{"enrolment": hex}`: an enrolment the organisation key signed, read and verified (§3), refused for a node already enrolled (`enrolment_duplicate`), kept |
| `GET /v1/fleet/enrolments` | every enrolment held, with its node, role, label, span, digest and the records held of it |
| `POST /v1/fleet/records` | `{"records": [...]}`: each record (§4.1), in order, verified as §7.2 steps 3 to 5 verify it against the enrolments and the records held, with no period, a descriptor held before it or earlier in the request; each held record is kept, and the answer gives each record's kind and identity or its refusal |
| `GET /v1/fleet/rooms/{room}/hops` | the per-hop records held of a room, each `record` and `ledger` (§8) |
| `POST /v1/fleet/reports` | `{"start", "end"}`: the report of the period over the enrolments whose span meets it and the records held in it, with the descriptors those name, signed, verified as §7 verifies it, and kept with its bundle |
| `GET /v1/fleet/reports` | every report kept, with its period |
| `GET /v1/fleet/reports/{digest}` | the report's bundle and its outcome |
| `POST /v1/fleet/verify` | a bundle: its outcome (§7.3) |
Each refusal names its reason and place. The service writes every file whole, appends records and
never rewrites one, holds a lock on its directory so that one process writes it, and on start reads
every enrolment, record and report back and verifies each again. It has no accounts: an enrolment
is accepted only when the organisation key signed it, and a record only when it verifies under an
enrolled node's key, whoever submits either. The service listens on a loopback address unless it
is told otherwise; who may reach it is decided in front of it.
`wai-fleet collect` reads a node's published summary file, a local file or an `http` or `https`
address answering with its lines, and submits each line to a running service as a `summary`
record.
The operations, named by the toolchain's rule (an operation `noun.verb`, its command line the words
in order):
| operation | command line | route | effect |
|---|---|---|---|
| `enrolment.create` | `wai enrolment create <node> --role <role> --key <file> [--label L] [--from µs] [--until µs] [--out F] [--target <name\|url>]` | `POST /v1/fleet/enrolments` with `--target` | writes a file; with `--target`, the service |
| `enrolment.list` | `wai enrolment list [--target <name\|url>]` | `GET /v1/fleet/enrolments` | reads |
| `report.create` | `wai report create [--target <name\|url>] --start µs --end µs` | `POST /v1/fleet/reports` | the service |
| `report.export` | `wai report export <digest> [--target <name\|url>] [--out F]` | `GET /v1/fleet/reports/{digest}` | writes a file |
| `receipt.verify` | `wai verify <bundle>` | `POST /v1/fleet/verify` | reads |
`--target` names the fleet service as every operation that acts on a service names one: a
target's name (`wai target add`) or the service's address; the three that need a service take the
target in use (`wai target use`) when none is named, and `enrolment.create` never does. A read-only
target refuses `enrolment.create` and `report.create`. A request carries the key `wai auth login`
kept for the service's origin, or `WAI_TOKEN`'s, and only over `https://` or to this machine.
Each keeps the toolchain's agent rules: `--json` writes its JSON envelope; each that writes takes
`--dry-run`, which writes nothing, sends nothing and says what it would; one that changes the
service, or would replace a file, waits for `--yes` (`--confirm`, deprecated, is the same), and
without it exits 4 with the proposed change and the command that confirms it.
## 10. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `enrolment_invalid` | §3.3, §7.2 step 2 |
| `enrolment_duplicate` | §3.4, §7.2 step 2 |
| `wrong_organisation` | §7.2 step 2 |
| `report_invalid` | §6.4, §7.2 step 1 |
| `signer_not_enrolled` | §7.2 step 4 |
| `role_mismatch` | §3.4, §7.2 step 4 |
| `descriptor_missing` | §4.5, §7.2 step 4 |
| `outside_enrolment` | §4.5, §7.2 step 4 |
| `line_unsourced` | §5.5, §7.2 step 6 |
| `unmetered_summed` | §5.4, §7.2 step 6 |
| `report_mismatch` | §7.2 step 6 |
A verifier of this extension also raises, with the meaning this text gives them: `outside_period`
(§4.5, §7.2 steps 2 and 4), `record_duplicate` and `record_overlap` (§4.6, §7.2 step 5) and
`citation_mismatch` (§5.5, §7.2 step 6), which receipt-ledger §10 adds; `classes_summed` (§5.4,
§6.4, §7.2 steps 1 and 6), `energy_invalid` and `record_invalid` (§4.2, §4.3), `ledger_invalid`
and `ledger_mismatch` (§4.3), which live-metering §9 adds; and `truncated`, `malformed` and
`unknown_version` for what does not read, and `bad_key` and `bad_signature` for its signature.
## 11. Conformance
[`live-conformance/fleet/`](../live-conformance/fleet/) holds the corpus, in one file,
`vectors.json`, written by `wai_fleet_vectors gen` from fixed seeds and from records taken from
[`live-conformance/metering/`](../live-conformance/metering/): the lesson room's descriptor and its
per-hop records, of three classes, with their ledgers; and the relay room's descriptor with the
per-hop record the reference relay signed, which states the unmetered marker. The summaries are
written by the generator in the reference relay's form, and `wai-fleet`'s tests read each one with
the relay's own types and require its bytes, re-serialised, to be the line's. Each vector is a
bundle with the outcome §7.3 gives it:
- **Accepted**: a week of a fleet of two relays cascading, a compositor and an ingest gateway, with
a room service enrolled and silent, whose report states an unmetered scope of sessions, a room
whose hops are of three classes, one row each, and a room whose relay's figure is the unmetered
marker, so that the fleet's hops are partial; a fleet whose every figure is metered, whole; a
fleet of two relays whose every figure is the unmetered marker; and a fleet enrolled and silent,
with no line.
- **Refused**: each reason of §10 at least once, each vector a control of one defect, among them a
report that drops an unmetered figure and one that counts it as a metered figure of zero joules;
and each reason it reuses.
`wai_fleet_vectors verify` and the test `tests/fleet.rs` check that the file regenerates byte for
byte, that `wai-rs` gives every vector its outcome, that every reason of §10 and every reused code
refuses a vector, and that §10 lists exactly the reference's codes. `live-conformance/fleet_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`), reads
per-hop records and their ledgers with the second implementation of live metering
(`live_metering_verify.py`), recomputes every line, row and citation itself, gives every vector the
same outcome, 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.
## 12. Security and privacy considerations
- **Who signs what.** Each node signs its own records, and the organisation signs only who its
nodes are and what their records add up to. An organisation cannot report a session no relay
summarised, a byte no record states, or a figure in a class its node did not sign: a verifier
recomputes every line from the records.
- **What a report rests on.** A report covers the records its bundle holds, and nothing proves a
bundle holds every record a node signed: a node that does not contribute, or an organisation
that leaves a record out, makes a report that is smaller and still verifies. The enrolments the
report lists show which nodes said nothing in the period (silent, §7.3), and a buyer or an
auditor who holds a node's own summary file checks it against the report's citations.
- **Energy.** A row states figures as their nodes signed them, with their class: attributable, and
its sum recomputable, not true (energy-measurement §5). `max_ppm` bounds the declared part of a
sum's uncertainty and says nothing of a class's undeclared systematic error. An unmetered figure
is counted, never summed as zero, so a fleet that meters some of its nodes reports that it does.
- **Keys.** The service holds the organisation key, since it signs reports. An enrolment signed with
that key elsewhere can be held by any copy of the service; one who holds the key can enrol any
node, so it is kept as the ledger's seller key is, readable by its owner alone.
- **What a bundle reveals.** A summary records its session's remote address and every track it
carried, and a bundle holds summaries whole, since their signatures cover every byte. A bundle
therefore reveals the network addresses of the fleet's clients as its relays recorded them, and it
travels only to those the organisation would show its relays' records to. The report itself names
nodes, rooms and record identities, and no client.