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