enrolment.create, enrolment.list, report.create and report.export join the catalogue as the other operations are: placed on the client against a fleet service, with their effect, toolset, input and output schemas, receipt and the features they need (fleet for the two that sign or verify). wai enrolment|report run through the door, and the fleet's command module keeps the agent rules of dx/envelope.rs instead of its own copy of them: the envelope with --json, every file through out.rs and every request to the service named in a dry run's plan and not sent (out::would_send), and --yes before the service is changed or a file replaced, exit 4 off a terminal with the proposal and the confirming command. A catalogue entry now says when it waits for confirmation (Op::confirms: never, a file the command line did not name, any file, the target or a file, always), and the rules refuse a change to a target that does not wait and a read that does. The export carries it and the agent flags (AGENT_FLAGS), from which wai --help is written: --yes is the one confirmation flag, and --confirm, which wai conform, wai setup-agent and the fleet took, is its deprecated spelling, still accepted, so named in help, in every confirming operation's help and in the export. wai conform confirms through the same module. wai verify's entry names the conformance results and fleet bundles it reads. Co-Authored-By: Claude Opus 5.5 <[email protected]>
35 KiB
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 §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 thefleetmodule ofwai-rs(featurefleet) and the servicewai/wai-fleet; the corpus islive-conformance/fleet/, and its second implementation islive-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 (§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 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 §5): a
signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable.
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 itsend(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, 3Estimator, 4OnChipCounter, 5CalibratedInstrument), 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); 2hops, 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 §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:
roleis 1 to 6;fromis belowuntil;labelis at most 64 bytes of UTF-8 and holds no control character (U+0000 to U+001F and U+007F);nodeis notorganisation.
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 §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 thet0of the descriptor among the records whose digest is itsroom. A per-hop record whose room has no such descriptor is refuseddescriptor_missing, and one whoset0+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_duplicatewhen both have one kind and one identity;record_overlapwhen both are summaries of one relay with onesessionand oneopened_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:
unmeteredcounts 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; andmax_ppm, the largestrelative_ppmamong 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 withinmax_ppmof 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:
startis belowend;- each list is in the strictly ascending order §6.1 gives it, and each record's
kindis 2, 10 or 11; - each node line's
roleis 1 to 6; - each row's
scopeis 1 or 2, itstag2 to 5, itsfiguresabove 0, and itsmax_ppm0 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:
-
The report (§6.4), at
report:truncated,malformed,unknown_version,classes_summed,report_invalid,bad_key,bad_signature. -
Each enrolment, in order, at
enrolmentand its index: it reads and its signature verifies (§3.3:truncated,malformed,unknown_version,enrolment_invalid,bad_key,bad_signature); itsorganisationis the report's (wrong_organisation); itsnodeis no earlier enrolment's (enrolment_duplicate); and its span meets the report's period,frombelow the period'sendand the period'sstartbelowuntil(outside_period). -
Each record, in order, at
recordand its index: it reads and its signature verifies, by its kind (§4.1 to §4.4, with their codes). -
Each summary and per-hop record, in order, at
recordand its index: its signer is thenodeof an enrolment (signer_not_enrolled); its kind and itsroleare 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). -
Across records, at
recordand the index of the later (§4.6):record_duplicate,record_overlap. -
The figures, recomputed from the bundle as §5 computes them, with the report's
recordsandenrolments(§7.1), and compared in this order:- 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
citedis 0:line_unsourced, atreport,nodesorrooms, and the line's index; - a row of a line or of the totals whose
joulesis 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 atreportandtotals; - a line or the totals whose
unmeteredfor a scope is below the unmetered markers of its records (§5.4):unmetered_summed, at the same places; - a line whose
citedorcitesis not the records it came from:citation_mismatch, at the line's place; - 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, atreport.
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.
- 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
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
recordsname the same records alike. - A per-hop record is carried with its ledger, in the members a ledger's
roomsource holds inhops(receipt-ledger §5.1: eachrecordandledger). 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 URL] |
POST /v1/fleet/enrolments with --target |
writes a file; with --target, the service |
enrolment.list |
wai enrolment list --target URL |
GET /v1/fleet/enrolments |
reads |
report.create |
wai report create --target URL --start µs --end µs |
POST /v1/fleet/reports |
the service |
report.export |
wai report export <digest> --target URL [--out F] |
GET /v1/fleet/reports/{digest} |
writes a file |
receipt.verify |
wai verify <bundle> |
POST /v1/fleet/verify |
reads |
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/ 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/: 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_ppmbounds 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.