open-standards/wai/extensions/fleet.md
Transaction Science f6bfd71ca9
Some checks failed
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m7s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m44s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m46s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 2m41s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 19s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 23s
wai-key-proxy / does this change touch it (push) Successful in 1m54s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m51s
WAI operation catalogue / does this change touch it (push) Successful in 3m25s
wai key · Windows / does this change touch it (push) Successful in 1m58s
wai-fleet service / does this change touch it (push) Successful in 2m22s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m56s
wai-audit collector and player integration / does this change touch it (push) Successful in 3m34s
wai-ledger service / does this change touch it (push) Successful in 1m56s
wai-sink-android native sink library / does this change touch it (push) Successful in 1m28s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m26s
WAI Studio / does this change touch it (push) Successful in 1m32s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m32s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 5m53s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m50s
wai-fleet service / service · macos (push) Successful in 1m21s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m43s
wai-ledger service / service · macos (push) Successful in 1m48s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m59s
WAI Studio / Studio in Chromium and Firefox · macos (push) Failing after 2m51s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Failing after 14m14s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 6m29s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 31s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
wai key · Windows / wai key and the toolchain's tests · windows (push) Failing after 1m40s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Failing after 1m51s
wai-key-proxy / proxy · linux (push) Successful in 1m25s
wai-ledger service / service · linux (push) Successful in 2m16s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 3m46s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m14s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 6m33s
wai-fleet service / service · linux (push) Successful in 7m5s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 11m35s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m6s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 5m44s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 7m52s
wai target and wai auth: a named service and a kept key, and one --target on every operation that acts on a service
- wai target add|use|list|remove (target.create, .update, .list, .delete): a name for a service's
  address, kept in the project's wai.json (its new targets member, in the schema) or the person's
  own list, never with a key; --read-only refuses every change (exit 3, target-read-only).
- wai auth login|status|logout (credential.create, .list, .delete): the key read from standard
  input, kept for its origin in the keychain on macOS or a file only its owner reads; WAI_TOKEN
  ahead of it; sent as a bearer over https:// or to this machine alone; never in wai.json, a
  list, output, a plan, a log or an error, a service's echo of it redacted.
- --target is catalogue::TARGET wherever it is taken, on operations placed off this machine: the
  fleet's four take a target's name or an address; an unknown name is exit 5, target-not-found.
- The forty operations of the room service's API answer to their mechanical form, sent to their
  route under the target (--query, --body), a change waiting for --yes, a dry run sending nothing.
- wai mcp --target serves them as tools, changes only to a target on this machine or with
  --allow-write; never with --read-only or to a read-only target.
- wai stage refine --toward, --target its deprecated spelling there (wai/DEPRECATIONS.md).
- tests/cli_target.rs, against a stand-in service; ops.json and Studio's actions regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 09:39:06 -05:00

36 KiB
Raw Permalink Blame History

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 the fleet module of wai-rs (feature fleet) and the service wai/wai-fleet; the corpus is 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 (§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 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 §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 §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/ 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_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.