The independent verifier's reading of the text found that a grant may narrow a publishing role to publish nothing and keep its chat: §8.1 gains rule 4 over grants, held at the first record that puts a minor's publishing grant into effect. §2, §3.4, §3.6, §5.2, §6.2, §7.3 and §9.1 now pin the reading orders and the attribution of claims, and the corpus covers the rules it did not reach. live-recording §4.4 step 5 refuses a package of a room whose recording policy has mode 0 (recording_not_permitted); its corpus gains room_never_recorded and live_record_verify.py the check. joule-moq's grant head reads the minor attribute, and joule-moq-relay's DVR store keeps nothing of a room once a session proves a minor's publishing grant of it, as it does for a sealed room, across a restart; tests/dvr.rs shows it. live-conformance/age_assurance_verify.py is the second implementation, written from the text: it shares no code with wai-rs, verifies every RSASSA-PSS in its own arithmetic, and catches every mutated copy and each of its own dropped checks. The registry export, SPEC §5, the CHANGELOG, the corpus exclusion reason and the wai-conformance step follow. Co-Authored-By: Claude Opus 5.5 <[email protected]>
828 lines
52 KiB
Markdown
828 lines
52 KiB
Markdown
# WAI Extension: Live Recording
|
||
|
||
> Status: Draft. Registers the record `wai.live.recording`: a finished room session as one
|
||
> WAI package, and clips of it that prove what they are. The package is a directory of files
|
||
> and its listing, as a package of [packaging](packaging.md) §11 is; its components are the
|
||
> room (its descriptor, grants and key-log head, [live-rooms](live-rooms.md)), its events
|
||
> (each principal's chain whole, [live-events](live-events.md) §8.6), one component for
|
||
> each participant's take ([capture-binding](capture-binding.md)), and, optionally, the
|
||
> composite; and a signed **recording manifest** names each component by its digest, with
|
||
> the signer, the room and the session's span on the room clock. A recording verifier gives
|
||
> `complete` and `publishable` for each purpose their verdicts. A **clip** is a span of one
|
||
> take, or one window of several takes aligned on the room clock; its signed **clip record**
|
||
> carries, for each take, the descriptors of the chunks it covers and a range proof that
|
||
> they are leaves of a commitment the device made while it captured, so a verifier holding
|
||
> the clip, the take's commitments, the device's key and the room descriptor knows the
|
||
> clip's bytes are exactly those bytes of that take, unedited, without the take. The HLS and
|
||
> LL-CMAF presentation of a recording (§7) and the addressing of its chunks for a MoQ FETCH
|
||
> (§8.1) are mappings. A relay's DVR window of a room's tracks (§8.2 to §8.5) keeps them, with
|
||
> their receipts, for a viewer to seek back into by FETCH. No new transport and no new
|
||
> container. The reference implementation is
|
||
> the `live_record` module of `wai-rs` (feature `live_record`); the corpus is
|
||
> [`live-conformance/recordings/`](../live-conformance/recordings/), and its second
|
||
> implementation is `live-conformance/live_record_verify.py`.
|
||
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
|
||
>
|
||
> Texts read for this revision: RFC 6962 §2.1 and RFC 9162 §2.1.1, §2.1.3 and §2.1.4 (the
|
||
> Merkle tree hash, inclusion proofs and their verification); RFC 8216 §3.3, §4.3.2.2,
|
||
> §4.3.2.5, §4.3.2.6, §4.3.3.1, §4.3.3.5 and §7; `draft-pantos-hls-rfc8216bis-22` §3.1.2,
|
||
> §4.4.3.7, §4.4.3.8, §4.4.4.5 and §4.4.4.9; and `draft-ietf-moq-transport-22` (MOQT)
|
||
> §2.4.1, §3.2, §3.2.1, §8.2, §8.7, §9.11 and §9.20.9. The two drafts are Internet-Drafts,
|
||
> and a section number of either in this text is that revision's.
|
||
|
||
## 1. Scope
|
||
|
||
In scope: the recording package, its files and its components (§3); the recording manifest
|
||
and recording verification, with the verdicts of `complete` and `publishable` (§4); the clip
|
||
record, the range proof it carries, clip verification, and cutting a clip by chunks, by
|
||
bytes or by a window of the room clock (§5); a clip across takes, its time correspondence
|
||
and its bound (§6); the mapping of a recording onto HLS and LL-CMAF (§7); and the addressing
|
||
of a recording's chunks for a MoQ FETCH, with the DVR window a relay keeps of a room's tracks
|
||
and serves to FETCH (§8).
|
||
|
||
Out of scope: a transport; how a relay lays its DVR window out on disk; a muxer for a
|
||
master that is not CMAF; the composition itself, which `wai.live.compose` defines
|
||
(live-compose §8, a later extension); and what a party does with a verdict.
|
||
|
||
Relationship to the core spec: `wai.live.recording` is a **record** ([SPEC](../SPEC.md) §5):
|
||
a signed record a WAI tool emits and verifies. It names no payload format and is not
|
||
dispatchable.
|
||
|
||
## 2. Terms
|
||
|
||
- **Room**, **room clock**, **principal**, **event**, **chain**, **grant**, **room key**:
|
||
as in live-events §2 and live-rooms §2.
|
||
- **Take**, **device**, **master**, **chunk**, **descriptor**, **take tree**,
|
||
**commitment**, **offset**, **purpose**: as in capture-binding §2 and §4. *I* and *J* are
|
||
the room's commit interval and jitter in microseconds (capture-binding §2).
|
||
- **Session span**: the room-clock instants a recording covers and its signer vouches for,
|
||
`from` to `to`, both included.
|
||
- **Package**: the directory of files a recording is (§3).
|
||
- **Component**: a part of a package named by a kind, an `id` and a `sha256` (§3.2).
|
||
- **Head**: the commitment of a take a clip's proof is made against: one of the take's
|
||
`take_commit` events (capture-binding §5.1), its size and its root.
|
||
- **Part** (of a clip): the span of one take a clip covers (§5.1).
|
||
- **Cutter**: whoever makes a clip and signs its record.
|
||
|
||
Integers are little-endian. A digest is SHA-256 (FIPS 180-4), 32 bytes. Ed25519 is RFC
|
||
8032's, verified strictly (SPEC §7 *Signatures*).
|
||
|
||
## 3. The recording package
|
||
|
||
### 3.1 Files and the listing
|
||
|
||
A recording is a directory of these files and no other:
|
||
|
||
```
|
||
recording.wrm the recording manifest (§4)
|
||
room/descriptor.wrd the room descriptor
|
||
room/grant-<k>.wrg each room grant
|
||
room/key-log.json the head of the room authority's key log
|
||
events/<principal>.wev each principal's chain
|
||
takes/<take>/manifest.wtm a take's manifest
|
||
takes/<take>/chunk-<index>.bin each chunk of the take the package holds
|
||
takes/<take>/receipt-<k>.wur the store's upload receipts
|
||
takes/<take>/consent-<k>.wcn the consent records
|
||
composite/receipt-<digest>.wcr the composite's receipts, optionally (§3.5)
|
||
composite/… and the objects they name
|
||
LISTING
|
||
```
|
||
|
||
`LISTING` is the listing (SPEC §3.1) of every other file, and its SHA-256 is the **package
|
||
digest**, as a package of packaging §11 has its own. Every path is one a listing can carry.
|
||
`<principal>` and `<take>` are a key and a take id as 64 lowercase hexadecimal digits,
|
||
`<index>` a chunk's index as ten decimal digits, and `<k>` a position as four decimal
|
||
digits from `0000`.
|
||
|
||
### 3.2 Components
|
||
|
||
A package has these components, each the files below one directory:
|
||
|
||
| kind | component | `id` | directory |
|
||
|---|---|---|---|
|
||
| 1 | the room | `room` | `room/` |
|
||
| 2 | the events | `events` | `events/` |
|
||
| 3 | a take | the take id, 64 lowercase hexadecimal digits | `takes/<take>/` |
|
||
| 4 | the composite | `composite` | `composite/` |
|
||
|
||
A component's **`sha256`** is the SHA-256 of the listing (SPEC §3.1) of its files, each by
|
||
its path below the component's directory. That is SPEC §7.1's component identity for a
|
||
component of several files: an `id`, of SPEC §7.1's form, and the `sha256` of its bytes,
|
||
here serialised as the listing that SPEC §3.1 gives a set of several files, as a pin in the
|
||
form `wai.prior.set` covers one. No other per-component digest is defined.
|
||
|
||
### 3.3 The room and the events
|
||
|
||
The room component holds `descriptor.wrd`, the room descriptor (live-rooms §3); one
|
||
`grant-<k>.wrg` for each room grant (live-rooms §4), numbered from `0000` without a gap in
|
||
the order a room verifier is given them (live-rooms §9.1); and `key-log.json`, the head of
|
||
the room authority's key log that the room verifier is given (live-rooms §5), when the
|
||
descriptor names an origin. It holds no other file.
|
||
|
||
The events component holds one file for each principal whose chain the recording holds,
|
||
named by the principal's key: its chain whole, its records concatenated in its stream order
|
||
(live-events §8.6). A record's length follows from its own fields (live-events §3.1), so the
|
||
file splits into records with no framing; a file that does not split into records that read
|
||
(live-events §3.3), each of that principal, is not a chain. A room verifier is given the
|
||
chains in the order of their files' paths, ascending order of the principals' keys, and that
|
||
order is the chain index by which live-rooms §9.2's merged order breaks ties.
|
||
|
||
### 3.4 A take
|
||
|
||
A take component's directory is `takes/<take>/`, its `id` the take id (capture-binding
|
||
§3.2). It holds `manifest.wtm`, the take manifest (capture-binding §7); one
|
||
`chunk-<index>.bin` for each chunk of the master the package holds, the chunk's bytes; the
|
||
store's upload receipts in their order as `receipt-<k>.wur` (capture-binding §6.2); and the
|
||
consent records as `consent-<k>.wcn` (capture-binding §8.1). It holds no other file.
|
||
|
||
The chunk files are the take's **parts** (capture-binding §6.1), given to the take verifier
|
||
in the order of their indexes. A chunk the store never received has no file: the package
|
||
holds what was uploaded, and the take's `complete` verdict says whether that is every
|
||
committed chunk. The master is its chunks concatenated in index order (capture-binding
|
||
§4.1).
|
||
|
||
### 3.5 The composite
|
||
|
||
A recording **MAY** hold the room's composite: the objects of a composed track, each with
|
||
the composition receipt `WCR1` its compositor signed for it, which `wai.live.compose`
|
||
defines (live-compose §8). The component `composite` holds each receipt as
|
||
`receipt-<digest>.wcr`, `<digest>` the receipt's digest (SHA-256 over its bytes before its
|
||
signature, live-compose §8.1) in 64 lowercase hexadecimal digits, and the objects the
|
||
receipts name beside them. The recording manifest names the component by its `sha256` like
|
||
any other, so the listing binds each receipt's bytes and its name its digest. A recording verifier of this revision
|
||
checks that digest and reads nothing in the component: verifying a composition receipt is
|
||
live-compose §8.3's, a later extension, and this extension depends on nothing of it but the
|
||
name and digest of its record.
|
||
|
||
## 4. The recording manifest
|
||
|
||
### 4.1 Layout
|
||
|
||
When the session is over, the room authority, or a principal that held the record power over
|
||
the whole span (§4.3), signs a recording manifest, `WRM1`:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WRM1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `signer` | 32 | the key that signs the manifest |
|
||
| `from` | 8 | u64: the session span's first instant, room clock in microseconds |
|
||
| `to` | 8 | u64: its last instant |
|
||
| `claims` | 1 | the claims the manifest makes: bit 0 `complete` |
|
||
| `purposes` | 1 | the purposes it claims every take `publishable` for (capture-binding §2) |
|
||
| `region` | 2 | ISO 3166-1 alpha-2: where the package is kept |
|
||
| `components` | 2 + … | a u16 count, then each component: `kind` (1), the length of its `id` (1), the `id`, and its `sha256` (32) |
|
||
| `sig` | 64 | Ed25519 by `signer` |
|
||
|
||
The **digest** of a manifest is SHA-256 over its bytes before `sig`; its **signature** is
|
||
over the 19 bytes `wai:live-recording` (ASCII) and 0x01, followed by the bytes the digest
|
||
covers.
|
||
|
||
### 4.2 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given, as live-events §3.3 reads a record with
|
||
`WRM1` for `WEV1` (`truncated`, `malformed`, `unknown_version`), and refuses a manifest
|
||
that breaks any of these rules as `recording_manifest_invalid`:
|
||
|
||
- `claims` sets no bit above bit 0, and `purposes` none above bit 3; `from` is at most `to`;
|
||
`region` is two capitals `A`–`Z`;
|
||
- the first component is the room (kind 1, `id` `room`) and the second the events (kind 2,
|
||
`id` `events`); then come the takes, each of kind 3 with an `id` of 64 lowercase
|
||
hexadecimal digits, in strictly ascending order of `id`; then, last, at most one
|
||
composite (kind 4, `id` `composite`); and no other component.
|
||
|
||
It then refuses one whose signature does not verify as `bad_key` or `bad_signature`. Each
|
||
`id` the rules admit is of SPEC §7.1's form, and the order makes the manifest of one
|
||
package one sequence of bytes.
|
||
|
||
### 4.3 Who signs
|
||
|
||
The room authority (the descriptor's `authority`, live-rooms §3.1) may sign any span. Any
|
||
other signer **MUST** hold the **record** power (live-rooms §3.3) at every instant of the
|
||
span, from `from` to `to` inclusive.
|
||
|
||
A principal holds the power at room-clock instant *t* when, in the room state replayed to
|
||
*t* (live-rooms §9.3: every `join`, `grant_exchange` and `leave` of it that the room verifier
|
||
accepted with its `t` at or below *t*, in the merged order), it is present, and its grant in
|
||
effect has the record power, is not revoked (live-rooms §5), and covers *t*: `nbf` ≤
|
||
⌊(`t0` + *t*) / 10^6⌋ < `exp`. So the grant took effect, by a `join` or a `grant_exchange`,
|
||
at or before `from`, and nothing takes the power away before `to`: no `leave`, no
|
||
`grant_exchange` to a grant without it, and no expiry.
|
||
|
||
A signer that does not hold it throughout is refused `recording_signer_not_permitted`, and
|
||
the verifier states the **first instant** of the span at which it does not hold it: `from`
|
||
when it held no grant with the power there; otherwise the `t` of the `leave` or the
|
||
`grant_exchange` that took the power away, or the first instant its grant does not cover.
|
||
One code with the instant, not one code per cause: the power over the span is one property,
|
||
each way it lapses is already a record of the room (a lifecycle event, or a grant's window)
|
||
that the instant points at, and the instant is what a signer needs to know to seal a span it
|
||
did hold the power over. A span that fails several ways fails first where the instant says.
|
||
|
||
The record power is the power to start and stop a recording, and sealing the session is the
|
||
end of that act. Held only at the end, it would let a principal granted it just before the
|
||
end seal a session it never held the power to record. A recorder that gained the power
|
||
partway through signs a manifest whose span starts no earlier than its grant took effect;
|
||
the package still holds the room's chains whole (§3.3), and §4.4 step 6 holds every take it
|
||
holds to that span. A device needs no record power to commit to its take, and a device whose
|
||
grant has none cannot seal the room's recording.
|
||
|
||
A revocation in the key log is not bounded by a time (live-rooms §5): the room verifier
|
||
refuses every use of a revoked grant, so a head that revokes the signer's grant refuses the
|
||
recording at the room (§4.4 step 4), before this rule is reached.
|
||
|
||
### 4.4 Verification
|
||
|
||
A recording verifier is given a package's files by path, and an evaluation instant `at` in
|
||
seconds since 1970-01-01T00:00:00Z, which it gives each take verifier (capture-binding
|
||
§9.1). It runs these steps and stops at the first refusal, which it reports with the reason
|
||
and where:
|
||
|
||
1. **The listing.** `LISTING` is present and is the listing of every other file:
|
||
otherwise `listing_mismatch`, at `listing`.
|
||
2. **The manifest.** `recording.wrm` is present, otherwise `recording_manifest_invalid`,
|
||
and it reads (§4.2); at `manifest`.
|
||
3. **The components.** For each component of the manifest, in its order: its directory
|
||
holds a file, otherwise `component_invalid`, and the SHA-256 of its listing is its
|
||
`sha256`, otherwise `component_digest_mismatch`; at that component. Then every file is
|
||
`LISTING`, `recording.wrm`, or a file below a component's directory: otherwise
|
||
`component_unlisted`, at `listing`.
|
||
4. **The room.** The room component holds the files of §3.3 and no other, and each file of
|
||
the events component is a chain of the principal it names (§3.3): otherwise
|
||
`component_invalid`, at that component. The room verifier (live-rooms §9) is given the
|
||
descriptor, the grants, the key-log head (or none) and the chains: a refusal is the
|
||
recording's, at `room` with the room verifier's own place. A gap is not a refusal: the
|
||
steps go on, and §4.5 says what a gap fails. The manifest's `room` is the room id:
|
||
otherwise `wrong_room`, at `manifest`.
|
||
5. **The recording policy**: the room's recording policy (live-rooms §3.7) has a `mode` other
|
||
than 0, otherwise `recording_not_permitted`, capture-binding §12's code, at `manifest`: a room
|
||
that is never recorded has no recording, takes or no takes. **The signer** (§4.3): otherwise
|
||
`recording_signer_not_permitted`, at `signer` with the
|
||
first instant of the span at which it does not hold the record power. **The region**:
|
||
where the room's recording policy pins regions (live-rooms §3.7), `region` is one of
|
||
them, otherwise `store_outside_region`, capture-binding §6.3's code, at `manifest`.
|
||
6. **The takes.** For each take component, in the manifest's order: it holds the files of
|
||
§3.4 and no other, otherwise `component_invalid`; the take verifier (capture-binding §9)
|
||
is given the room as in step 4, the take manifest, the parts, the receipts, the
|
||
consents and `at`, and its refusal is the recording's, at `take` with the component's
|
||
index and the take verifier's own place; the take id it reads is the component's `id`,
|
||
otherwise `component_invalid`; and the take lies within the span, its start (the
|
||
`record_start` it answers) at or after `from` and its last held commitment at or before
|
||
`to`, otherwise `outside_span`; at that component. The records before `from` or after
|
||
`to` that the package holds are the room's chains whole, which the room verifier replays
|
||
the room from; the manifest states nothing of them.
|
||
7. **The verdicts** (§4.5).
|
||
|
||
### 4.5 Verdicts and outcome
|
||
|
||
**`complete`** holds when the recording holds every take of the session that starts within
|
||
the span, each whole, and every event the room verifier was offered: the first reason below
|
||
that applies.
|
||
|
||
1. `delivery_gap`: the room verifier holds a gap (live-rooms §9.4).
|
||
2. `take_omitted`: a take of the session whose `record_start` lies within the span has no
|
||
component. A **take of the session** is the take id (capture-binding §3.2) that a
|
||
`record_start` the room verifier accepted (live-events §4.9) names for its target, where
|
||
the room holds a `take_commit` of that target whose payload reads (capture-binding §5.2)
|
||
and names that take: a start its device answered.
|
||
3. For each take component, in the manifest's order: its `single-take` verdict fails with
|
||
`delivery_gap`, `commit_chain_broken` or `take_not_closed`, so its commitments are not
|
||
one unbroken chain from the opening commitment to the closing one: that reason; or its
|
||
`complete` verdict fails: that reason (capture-binding §9.4).
|
||
|
||
**`publishable`** for purpose *p* holds when every take is `publishable` for *p*: for the
|
||
first take component, in the manifest's order, whose `publishable` for *p* fails, that
|
||
reason.
|
||
|
||
A recording verification gives one outcome:
|
||
|
||
- **refuse**: the reason and where (§4.4);
|
||
- **verdicts**: the package digest; the manifest's digest; the room id; the verdicts of
|
||
`complete` and of `publishable` for each of the four purposes; each take's id and verdicts
|
||
(capture-binding §9.5); and whether the manifest **stands**: every claim it makes, and
|
||
`publishable` for every purpose it names, holds.
|
||
|
||
## 5. Clips
|
||
|
||
### 5.1 The clip record
|
||
|
||
A clip is a span of one take, or one window of several takes aligned on the room clock
|
||
(§6). A cutter signs a clip record, `WCL1`:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `magic` | 4 | `WCL1` (ASCII) |
|
||
| `version` | 1 | 1 in this revision |
|
||
| `room` | 32 | the room id |
|
||
| `window_in` | 8 | u64: the first room-clock instant the clip presents, in microseconds |
|
||
| `window_out` | 8 | u64: the instant it presents up to (excluded) |
|
||
| `parts` | 1 + … | a u8 count *k*, then each part as below |
|
||
| `signer` | 32 | the cutter's key |
|
||
| `sig` | 64 | Ed25519 by `signer` |
|
||
|
||
Each part:
|
||
|
||
| field | size | meaning |
|
||
|---|---|---|
|
||
| `device` | 32 | the device's key |
|
||
| `take` | 32 | the take id |
|
||
| `head` | 32 | the digest of the `take_commit` event whose commitment the proof is against |
|
||
| `size` | 8 | u64: that commitment's size |
|
||
| `root` | 32 | that commitment's root |
|
||
| `first` | 8 | u64: the index of the first chunk the part covers |
|
||
| `count` | 4 | u32: the number of chunks it covers |
|
||
| `trim_in` | 8 | u64: the bytes of the first chunk before the clip's bytes |
|
||
| `trim_out` | 8 | u64: the bytes of the last chunk after them |
|
||
| `chunks` | 64 × `count` | the descriptor of each chunk it covers (capture-binding §4.1), in index order |
|
||
| `proof_len` | 1 | the number of nodes in `proof` |
|
||
| `proof` | 32 × `proof_len` | the range proof (§5.3) |
|
||
|
||
The **digest** of a clip record is SHA-256 over its bytes before `sig`; its **signature**
|
||
is over the 14 bytes `wai:live-clip` (ASCII) and 0x01, followed by the bytes the digest
|
||
covers. A part's bytes are its chunks' bytes concatenated, less `trim_in` bytes at the
|
||
start and `trim_out` at the end; the clip's bytes are its parts'.
|
||
|
||
### 5.2 Reading and rules
|
||
|
||
A reader reads exactly the bytes it is given, as live-events §3.3 reads a record with
|
||
`WCL1` for `WEV1` (`truncated`, `malformed`, `unknown_version`), and refuses a record that
|
||
breaks any of these rules as `clip_record_invalid`:
|
||
|
||
- *k* is 1 to 16, and no take is named by two parts;
|
||
- in each part, `count` is at least 1; each chunk's `index` is `first` plus its position,
|
||
and each `len` is at least 1; `proof_len` is at most 128; `trim_in` is below the first
|
||
chunk's `len`, and `trim_out` below the last chunk's; and, with one chunk, `trim_in` +
|
||
`trim_out` is below its `len`;
|
||
- a part with a trim that is not 0 is the clip's only part;
|
||
- `window_in` is below `window_out`.
|
||
|
||
It then refuses one whose signature does not verify as `bad_key` or `bad_signature`.
|
||
|
||
**Partial chunks.** A clip whose range begins or ends inside a chunk carries that whole
|
||
chunk and states the bytes it leaves out, as `trim_in` and `trim_out`. A leaf commits to a
|
||
chunk's digest over all its bytes (capture-binding §4.2), so a part of a chunk can never be
|
||
proved on its own: with the whole chunk the proof stays exact, and the trims say which of
|
||
the proved bytes the clip presents. Refusing a range that does not fall on chunk boundaries
|
||
would make a byte range of the master, or a range request, unprovable unless it happened to
|
||
land on them. A trim is in bytes, which have no committed time within a chunk, so a trimmed
|
||
clip's window is its chunks' span (§5.4 step 3j), and trims are a single take's: an aligned
|
||
clip (§6) presents a window of time, not of bytes. A cutter that must not disclose the bytes
|
||
outside the range cuts on chunk boundaries, since the trimmed bytes travel with the clip.
|
||
|
||
### 5.3 The range proof
|
||
|
||
A part proves its chunks by one proof over the run of leaves they are, against the head.
|
||
With `D[n]` the take tree's leaves (capture-binding §4.2) under the head's size *n*,
|
||
`MTH` RFC 9162 §2.1.1's Merkle tree hash with the take tree's node, and *k* the largest
|
||
power of two below *n*, the proof of leaves *a* to *b* − 1 (0 ≤ *a* < *b* ≤ *n*) is:
|
||
|
||
```
|
||
RANGE(a, b, D[n]) = {} a = 0 and b = n
|
||
= RANGE(a, b, D[0:k]) : MTH(D[k:n]) b ≤ k
|
||
= RANGE(a − k, b − k, D[k:n]) : MTH(D[0:k]) a ≥ k
|
||
= RANGE(a, k, D[0:k]) : RANGE(0, b − k, D[k:n]) otherwise
|
||
```
|
||
|
||
where `:` appends. A run of one leaf is never split, so `RANGE(i, i + 1, D[n])` is RFC 6962
|
||
§2.1.1's `PATH(i, D[n])`, the inclusion proof RFC 9162 §2.1.3.1 generates, node for node.
|
||
A proof of a whole tree is empty. A proof has at most 2⌈log₂ *n*⌉ nodes, at most 128 for any
|
||
size a commitment can state, and every node of it is a node of the inclusion proof of a leaf
|
||
in the run.
|
||
|
||
A verifier given the leaves *L* of the run (*b* − *a* of them), the size *n* and the
|
||
nodes, used in order from the first, computes `ROOT(a, b, n, L)`:
|
||
|
||
```
|
||
ROOT(a, b, n, L) = MTH(L) a = 0 and b = n
|
||
= node(ROOT(a, b, k, L), next) b ≤ k
|
||
= node(next, ROOT(a − k, b − k, n − k, L)) a ≥ k
|
||
= node(ROOT(a, k, k, L[0 : k − a]),
|
||
ROOT(0, b − k, n − k, L[k − a :])) otherwise
|
||
```
|
||
|
||
where `next` is the next unused node, and `node` is the take tree's (staged-measurement
|
||
Appendix C). The proof verifies when no `next` is asked for past the last node, every node
|
||
is used, and `ROOT` is the head's root. For a run of one leaf this is RFC 9162 §2.1.3.2's
|
||
check of an inclusion proof, and gives its answer.
|
||
|
||
The size is the head's, which the device signed, never the cutter's: as for an inclusion
|
||
proof, the same nodes can reach one root under two sizes (staged-measurement Appendix C),
|
||
so a size the cutter chose would let it choose the leaves' positions. Under the head's size
|
||
the recursion fixes which subtree each node stands for; the leaf and node domains differ, so
|
||
no node passes for a leaf; and each leaf binds its chunk's index, capture range, length and
|
||
digest (capture-binding §4.2). A proof that verifies therefore shows, short of a collision of
|
||
SHA-256, that the run's descriptors are the head's leaves *a* to *b* − 1.
|
||
|
||
### 5.4 Verification
|
||
|
||
A clip verifier is given the room descriptor; the clip record; and, for each part in order,
|
||
the device's key, the take's commitments (its `take_commit` events, from its opening one to
|
||
the head, in the order of the take's chain), and the bytes of each chunk the part covers,
|
||
each present or absent. It is not given the take manifest, any other chunk, the upload, or
|
||
the room's other events. It runs these steps and stops at the first refusal, which it
|
||
reports with the reason and where:
|
||
|
||
1. **The descriptor** reads (live-rooms §3.4): its refusal, at `descriptor`. The room id is
|
||
its digest, and *I* and *J* are its recording policy's (live-rooms §3.7).
|
||
2. **The record** reads (§5.2), at `clip`. Its `room` is the room id, otherwise
|
||
`wrong_room`; and the verifier is given one input for each part, otherwise
|
||
`clip_record_invalid`; at `clip`.
|
||
3. **Each part**, in order, steps (a) to (k):
|
||
- **(a)** The key given is the part's `device`: otherwise `clip_device_mismatch`, at the
|
||
part.
|
||
- **(b)** Each commitment given reads (live-events §3.3), names the room
|
||
(`wrong_room`), is the device's (`clip_device_mismatch`), verifies (`bad_key`,
|
||
`bad_signature`), is a `take_commit` whose payload reads (capture-binding §5.2,
|
||
`commit_malformed`), and names the part's `take` (`clip_take_mismatch`); at that
|
||
commitment.
|
||
- **(c)** The commitments are a chain (capture-binding §5.3): the first is an opening one,
|
||
and each later one has the `prev`, `first` and `n` that follow its predecessor, which
|
||
is not a closing one, and is itself not an opening one. A later one whose `n` is more
|
||
than one above its predecessor's is `delivery_gap`; any other break is
|
||
`commit_chain_broken`. Then each extends its predecessor (capture-binding §4.3),
|
||
otherwise `root_not_extending`. At the first commitment that fails, the chain checked
|
||
over all of them before any extension, as capture-binding §9.4 orders them. None given
|
||
is `commit_chain_broken` at the part.
|
||
- **(d)** The last commitment given is the head: its digest is `head`, and its size and
|
||
root are the part's `size` and `root`. Otherwise `tree_head_not_committed`, at the
|
||
part: a tree the take never committed proves nothing of it.
|
||
- **(e)** `first` + `count` is at most `size`: otherwise `clip_out_of_range`, at the part.
|
||
- **(f)** The proof (§5.3) shows the part's descriptors are the head's leaves `first`
|
||
onward, under `size`: otherwise `clip_not_included`, at the part.
|
||
- **(g)** Every commitment's offset is within *J* of the opening one's, *o* (capture-binding
|
||
§9.4 `single-take` 6): otherwise `clock_unstable`, at that commitment.
|
||
- **(h)** The chunks tile their capture, each `start` below its `end` and each `start` the
|
||
previous chunk's `end` (`capture_discontinuous`), and none spans more than *I*
|
||
(`chunk_too_long`) (`single-take` 7, 8); at the part.
|
||
- **(i)** With each chunk mapped by *o* (capture-binding §9.3) and *t* the room-clock
|
||
instant of the commitment that adds it (the one whose `first` ≤ `index` < `size`): no
|
||
chunk has *t* + *J* below its mapped end (`commit_early`), and then none has *t* above
|
||
its mapped end plus *I* plus *J* (`commit_late`) (`single-take` 9, 10); at the part.
|
||
- **(j)** The window: with the part's **span** the first chunk's mapped `start` to the last
|
||
one's mapped `end`, the span covers the window, the first chunk ends after
|
||
`window_in` and the last begins before `window_out`; and a trimmed part's window is
|
||
its span. Otherwise `clip_window_invalid`, at the part.
|
||
- **(k)** The bytes: for each chunk in order, its bytes are given (`clip_chunk_missing`)
|
||
and are that chunk's, their digest and length its descriptor's; bytes that are
|
||
another covered chunk's are `chunk_reordered`, any others `chunk_altered`; at that
|
||
chunk. Bytes given past the last chunk are `chunk_extra`, at the chunk past it.
|
||
|
||
Steps (b) to (k) are `single-take` over the span: the rules of capture-binding §9.4
|
||
`single-take` that hold of the chunks the clip covers, checked from the commitments up to the
|
||
head. A clip does not need rule 4 (closed), since it may be cut while the take runs (§8);
|
||
nor rule 5 (the manifest), since inclusion binds its chunks to the head instead; nor rule
|
||
9's clauses on the start and the stop, whose events it is not given. A take whose
|
||
`single-take` fails only at chunks outside a span, as by a late commitment, still gives
|
||
clips of that span that verify.
|
||
|
||
A clip that passes is **accepted**, and the verifier states: the record's digest and
|
||
signer; the window; the bound (§6.2); and for each part its take, `first`, `count`, the
|
||
take's offset *o*, its span, the window on its device's clock (§6.1), the length and SHA-256
|
||
of the bytes it presents, and its FETCH address (§8). Where the take's `single-take` holds, an
|
||
accepted clip's descriptors are its manifest's at those indexes, since rule 5 binds the
|
||
manifest's chunks to every commitment's root; the clip verifier does not need the manifest.
|
||
A clip over chunks a moderation decision restricts is verified as
|
||
[live-moderation](live-moderation.md) §8.3 has it: step (k) passes over the restricted
|
||
chunks, whose descriptors the proof still covers.
|
||
|
||
### 5.5 Cutting a clip
|
||
|
||
A cutter holds a take's manifest, its commitments and the chunks it covers. It picks a head:
|
||
the take's closing commitment, or, while the take runs, any commitment whose size covers the
|
||
range. It cuts:
|
||
|
||
- **by chunks**: `first` and `count`, with no trim;
|
||
- **by bytes**: a byte range [*x*, *y*) of the master, with chunk *i* at the master's bytes
|
||
from the sum of the `len` of the chunks before it: the chunks from the one holding byte
|
||
*x* to the one holding byte *y* − 1, `trim_in` *x* less the first one's offset, and
|
||
`trim_out` the last one's end less *y*;
|
||
- **by a window** [*w₀*, *w₁*) of the room clock: in each take, the chunks from the one whose
|
||
mapped capture holds *w₀* to the one whose mapped capture holds *w₁* − 1, with no trim.
|
||
|
||
A clip cut by chunks or bytes presents its chunks' span as its window. The proof is §5.3's
|
||
over the manifest's descriptors up to the head's size; the cutter signs the record.
|
||
|
||
## 6. Across takes
|
||
|
||
### 6.1 Time correspondence
|
||
|
||
An aligned clip has a part for each take it spans, over one window of the room clock. Each
|
||
take is placed on the room clock by its own mapping, the offset *o* of its opening
|
||
commitment (capture-binding §9.3): a room-clock instant *t* in the window is the device-clock
|
||
instant *t* − *o_p* in part *p*, in the chunk whose capture range holds it, at its position
|
||
in that chunk and in the master (capture-binding §10). Two parts *p* and *q* correspond at
|
||
*t*: device instant *d* in *p* is *d* + *o_p* − *o_q* in *q*. The verifier states each part's
|
||
window on its device's clock, [`window_in` − *o_p*, `window_out` − *o_p*).
|
||
|
||
### 6.2 The bound
|
||
|
||
A part's mapping is held, over every commitment up to its head, to within *J* of the offset
|
||
the device measured at that commitment (step 3g), and each chunk it covers was committed
|
||
within [−*J*, *I* + *J*] of its mapped end on the room clock (step 3i). The clip states its
|
||
**bound**, `bound_us`: *J* for a clip of one take, its mapping's to the room clock; and
|
||
2*J* for an aligned clip, between any two of its parts, each strays by *J* from the room
|
||
clock as its device measured it. A sink that presents the parts of an aligned clip together
|
||
aligns them within that bound of what the devices measured. The measurement itself, and each
|
||
commitment's `t`, are their authors' statements (live-events §11), held by the room's other
|
||
records as capture-binding §14 says.
|
||
|
||
## 7. HLS and LL-CMAF
|
||
|
||
This section is a mapping: what a standard HLS client plays of a recording, with no new
|
||
format. Where a take's master is CMAF (ISO/IEC 23000-19), cut as capture-binding §4.1 cuts
|
||
it, chunk 0 is the CMAF header and the first CMAF segment, and each later chunk one CMAF
|
||
segment, so the take's chunk files are already an HLS presentation of fragmented MPEG-4
|
||
segments (RFC 8216 §3.3; `draft-pantos-hls-rfc8216bis-22` §3.1.2):
|
||
|
||
- **One Media Playlist per take.** `EXT-X-VERSION` is 6 or higher, as `EXT-X-MAP` without
|
||
`EXT-X-I-FRAMES-ONLY` requires (RFC 8216 §4.3.2.5, §7; the draft's §4.4.4.5).
|
||
`EXT-X-TARGETDURATION` is the longest chunk's `end` − `start`, in seconds rounded up
|
||
(RFC 8216 §4.3.3.1), never above *I* by `single-take` rule 8. `EXT-X-PLAYLIST-TYPE` is
|
||
`VOD` for a closed take and `EVENT` while it runs, segments added only at the end (RFC
|
||
8216 §4.3.3.5), with `EXT-X-ENDLIST` once the take is closed.
|
||
- **The initialization section** is `EXT-X-MAP` with `URI` chunk 0's file and `BYTERANGE`
|
||
`<h>@0` (the draft's §4.4.4.5), *h* the length of chunk 0's top-level boxes (ISO/IEC
|
||
14496-12) before its first `styp` or `moof`. Chunk 0's segment is then
|
||
`EXT-X-BYTERANGE:<len − h>@<h>` of the same file (RFC 8216 §4.3.2.2), and each later
|
||
chunk its own file whole.
|
||
- **Durations and dates.** Each segment's `EXTINF` is its chunk's `end` − `start` in
|
||
seconds. The first segment carries `EXT-X-PROGRAM-DATE-TIME` (RFC 8216 §4.3.2.6), the
|
||
instant `t0` + `start` + *o* of the room clock in UTC; a gap-free take needs no other.
|
||
- **Events.** The room's events ride the playlist as live-events §8.3 maps them, as
|
||
`EXT-X-DATERANGE`, so a marker lands at its instant in the master (capture-binding §10).
|
||
- **Several takes.** Each take is its own Media Playlist. An HLS client presents one
|
||
rendition of a medium at a time; one that shows two takes together aligns them by
|
||
`EXT-X-PROGRAM-DATE-TIME`, within §6.2's bound.
|
||
- **LL-CMAF.** A CMAF segment holds CMAF chunks, each a `moof` and its `mdat`. A
|
||
low-latency playlist of a running take lists them as `EXT-X-PART` with `BYTERANGE` into
|
||
the chunk's file (the draft's §4.4.4.9), with `EXT-X-PART-INF` and
|
||
`EXT-X-SERVER-CONTROL` (§4.4.3.7, §4.4.3.8). The take tree commits to whole chunks, so a
|
||
part is proved only as part of its chunk, once the commitment that adds the chunk is
|
||
held, within *I* + *J* of the chunk's end (`single-take` 10): a sink that verifies
|
||
presents parts ahead of that and holds its verdict until the chunk is committed.
|
||
|
||
A master that is not CMAF is played this way only through a muxer for its codec, which this
|
||
extension does not define. What this revision implements of this section: nothing. The
|
||
corpus's masters are not CMAF, the reference has no muxer for a device's master (packaging
|
||
§7's CMAF writer writes a floor's AV1 fragments), and it writes no playlist.
|
||
|
||
## 8. DVR over MoQ FETCH
|
||
|
||
This section fixes how a recording's chunks are addressed for a FETCH (§8.1), and what a
|
||
relay that keeps a window of a room's tracks, a **DVR window**, does with it: what it keeps
|
||
and evicts (§8.2), how it answers a FETCH from it (§8.3), what moderation withholds from it
|
||
(§8.4), and how it keeps it across a restart (§8.5); and the window's presentation over HTTP
|
||
(§8.6).
|
||
|
||
### 8.1 Addressing a take
|
||
|
||
- **The track.** A recorded take is one track. Its Track Namespace (MOQT §2.4.1) has three
|
||
fields: the room id and the device's key, each as 64 lowercase hexadecimal digits, and
|
||
`take`; its Track Name is the take id as 64 lowercase hexadecimal digits. Every byte is one
|
||
of `a`–`z` and `0`–`9`, as in live-rooms §6.1's names, and its fields' 196 bytes are within MOQT §8.7's limit.
|
||
- **Groups and objects.** Group *g* is chunk *g*, and its one object, Object ID 0, is the
|
||
chunk's bytes whole: the unit a leaf commits to. A group's Location (MOQT §8.2) is
|
||
(*g*, 0).
|
||
- **A clip's part** is fetched by a FETCH (MOQT §3.2, §9.11) of that track whose
|
||
`LOCATION_FILTER` (MOQT §9.20.9) has StartGroup `first`, StartObject 0, EndGroupDelta
|
||
`count` − 1 and no EndObject, so every object of the end group: the range is inclusive
|
||
(MOQT §3.2), and the groups arrive in the requested order (MOQT §3.2.1). The clip
|
||
verifier states this address for each part (§5.4).
|
||
- **The commitments** are events of the device's events track (live-rooms §6.1), whose
|
||
group is the minute of room clock an event falls in (live-events §8.1). A FETCH of its
|
||
groups from the opening commitment's to the head's holds them, among the device's other
|
||
events; the clip verifier is given the take's `take_commit` events alone.
|
||
- **A running take.** The groups below the latest commitment's size are committed; a group
|
||
at or past it is a chunk no commitment has added yet, which a sink holds unverified until
|
||
one does (§5.5: a clip of a running take is proved against the commitment that covers it).
|
||
|
||
A take's track kept in a DVR window is kept and answered as any other track (§8.2, §8.3), so
|
||
a clip's part is a FETCH the window answers.
|
||
|
||
### 8.2 The window
|
||
|
||
A relay **MAY** keep a window of the tracks of a room: its media tracks with their receipts
|
||
tracks, its events tracks, its catalogs and its takes.
|
||
|
||
- **What it keeps.** A group, once the relay holds it whole: its first object arrived (or
|
||
a Prior Object ID Gap says the objects before do not exist, MOQT §10.9), none of its
|
||
subgroup streams is open or was reset, and its end is known. The objects are kept with
|
||
every field a fetch stream carries (MOQT §11.4.1).
|
||
- **Its bounds.** An age, counted from when a group's first object reached the relay, of at
|
||
most 86 400 seconds: 24 hours, the start-over buffer a live-to-VOD harvest clips from;
|
||
and a byte budget over the window's tracks together. Past either, the oldest group goes
|
||
first, whole. Group *g* of a media track and group *g* of its receipts track
|
||
([moq-streaming-format](moq-streaming-format.md) §4.1) go together, so that every group
|
||
the window serves comes with its receipts and a viewer checks a recorded object exactly as
|
||
it checks a live one ([jwp-receipts](jwp-receipts.md) §4.1). A group that becomes whole
|
||
after the window has moved past its group number is not kept.
|
||
- **Where.** The window is media and events the relay keeps beyond delivery, so the room's
|
||
region pin (live-rooms §3.7) binds where it is stored.
|
||
- **Who reads it.** A FETCH or fill below a track's live edge is served only to a grant
|
||
with the seek power (live-rooms §10.6).
|
||
- **A room it never keeps.** A room with a confidentiality record is never recorded, and a
|
||
relay keeps nothing of its tracks beyond delivery ([confidentiality](confidentiality.md)
|
||
§9). A relay knows such a room by its first sealed object, one whose Immutable Properties
|
||
carry `SECURE_OBJECT_KEY_ID` (confidentiality §4.3), which it reads without a key. It
|
||
**MUST NOT** keep that object's group. From then on it keeps nothing of the room's tracks,
|
||
deletes what it kept of them before (the room's events and receipts tracks are in the clear,
|
||
and can be whole before its first sealed group), and remembers the room across a restart.
|
||
It still honours the moderation decisions it reads on the room's events tracks for what it
|
||
delivers, and does not keep them.
|
||
- **A minors' room.** A room in which a minor publishes is never kept either
|
||
([age-assurance](age-assurance.md) §8.4): from the moment a session proves a minor's
|
||
publishing grant of the room, the relay keeps nothing of its tracks, deletes what it kept, and
|
||
remembers the room across a restart, as for a room whose objects are sealed.
|
||
|
||
### 8.3 Answering a FETCH
|
||
|
||
- **Where from.** A FETCH whose range the relay's cache holds is answered from the cache.
|
||
Otherwise, when every group from its start to its end is held whole by the cache or the
|
||
window, is known not to exist (MOQT §10.8), or was evicted from the window, the relay
|
||
answers it from them; otherwise it asks upstream (MOQT §3.2.3). Groups come in the
|
||
requested order (MOQT §3.2.1), objects within a group in Object ID order. A FETCH of the
|
||
receipts track over the same range brings each group's receipts the same way.
|
||
- **What was evicted** is no longer known to the relay. The relay marks the evicted groups
|
||
of the range with an End of Unknown Range (MOQT §11.4.1.2, Serialization Flags `0x10C`)
|
||
through the last of them, then sends the groups it holds: a range partly evicted gets what
|
||
remains and the marker, and a range wholly evicted gets FETCH_OK and the marker alone. It
|
||
uses neither an unmarked gap nor an End of Non-Existent Range: an evicted object existed,
|
||
an unmarked gap would say it did not (MOQT §3.2.2), and MOQT §10.3 makes an object
|
||
that has left a cache an object of unknown state (§2.1 there).
|
||
- **Inside a group.** An End of Range moves to a later group than the entry before it: its
|
||
Group ID is coded as an object's Group ID Delta (MOQT §11.4.1.1, §11.4.1.2), the reading
|
||
the `joule-moq` codec and the MOQT -22 vectors of `joule-moq` take. So when objects of a
|
||
group cannot be served (withheld, §8.4; dropped, §8.5), one End of Unknown Range from the
|
||
group's start through the last of them goes first, and the group's objects after it
|
||
follow. The objects before the last such one are not sent.
|
||
- **A window whose end lies ahead.** A live-to-VOD harvest clips a window of at most 24 hours
|
||
whose end may lie in the future. Its past is a FETCH. The whole is one SUBSCRIBE whose
|
||
`LOCATION_FILTER` is the window, as an AbsoluteRange (MOQT §9.20.9), with
|
||
`FILL_PARAMETERS` (MOQT §3.4): the fill is answered from the cache and the window as a
|
||
FETCH is, and the subscription delivers the rest up to the window's end.
|
||
|
||
### 8.4 Moderation
|
||
|
||
What [live-moderation](live-moderation.md) withholds is withheld from the window, as §6.3
|
||
there has a relay withhold it in answer to a FETCH:
|
||
|
||
- **How the relay learns of decisions.** It reads the `decision` events (live-events §4.11)
|
||
on the room's events tracks, which it already forwards. It honours a decision whose record
|
||
verifies under its principal (live-events §3.2), whose principal owns the events track it
|
||
rides (live-rooms §6.1), and whose principal is a moderator it knows: a room key, or the
|
||
principal of a grant with the moderate power (live-rooms §3.3) that its publishing session
|
||
proved (live-rooms §10.5). The last decision it honoured on a target stands
|
||
(live-moderation §4.5), and the honoured decisions are kept with the window.
|
||
- **Deny and redact** withhold the target from every answer to every session but a
|
||
moderator's; **redact** also erases it from the window. **Allow** restores what deny
|
||
withheld. A withheld media object is the one whose receipt names it by its digest
|
||
(live-moderation §4.1); its receipts stay where they are (live-moderation §8.1). A withheld
|
||
event is left out of the events-track object that carries it, the object's other records
|
||
sent byte for byte; an object left with none is withheld whole. Either is withheld, never
|
||
replaced: each is an End of Unknown Range in its place (§8.3), and the decision that
|
||
restricts it says why.
|
||
|
||
### 8.5 Across a restart
|
||
|
||
A window kept across a restart is checked again before it is served. Every object is
|
||
checked against its receipt: a media object of a track with a receipts track against group
|
||
*g*'s receipts, as jwp-receipts §4.1 checks one at the door (the group receipt's signature
|
||
under the trust the relay holds for the namespace, the object's receipt at its position,
|
||
its Merkle proof to the signed root, its content hash); an events-track object's records each
|
||
under its principal (live-events §3.2). An object that does not verify, or whose stored
|
||
bytes do not hold, is dropped and counted, and never served: a FETCH that reaches it is told
|
||
End of Unknown Range (§8.3). How a relay lays the window out on disk is its own; the
|
||
reference relay's layout and its durability rule are in its README.
|
||
|
||
### 8.6 Over HTTP
|
||
|
||
The window is a time-shift buffer. In DASH (ISO/IEC 23009-1) its age is the MPD's
|
||
`timeShiftBufferDepth`. In HLS a live Media Playlist keeps the window's segments and
|
||
removes them from its start as the window evicts their groups; `EXT-X-PLAYLIST-TYPE`
|
||
`EVENT` (RFC 8216 §4.3.3.5), whose segments are added only at the end, fits only a window
|
||
that holds the session from its start and evicts nothing.
|
||
|
||
What this revision implements of §8.2 to §8.5: the reference relay, `joule-moq-relay`
|
||
(`joule-code/joule-code-rs/crates/joule-moq-relay`), keeps a window per namespace prefix
|
||
and its tests show each rule above. §8.6 is a mapping, and nothing writes it.
|
||
|
||
## 9. Reasons
|
||
|
||
The codes this extension adds, each with where it is raised:
|
||
|
||
| reason | raised at |
|
||
|---|---|
|
||
| `listing_mismatch` | §4.4 step 1 |
|
||
| `recording_manifest_invalid` | §4.2, §4.4 step 2 |
|
||
| `component_invalid` | §4.4 steps 3, 4 and 6 |
|
||
| `component_digest_mismatch` | §4.4 step 3; SPEC §7.1's code, for a component of a recording |
|
||
| `component_unlisted` | §4.4 step 3 |
|
||
| `recording_signer_not_permitted` | §4.3, §4.4 step 5, with the first instant of the span without the power |
|
||
| `outside_span` | §4.4 step 6 |
|
||
| `take_omitted` | §4.5 `complete` 2 |
|
||
| `clip_record_invalid` | §5.2, §5.4 step 2 |
|
||
| `clip_device_mismatch` | §5.4 steps 3a, 3b |
|
||
| `clip_take_mismatch` | §5.4 step 3b |
|
||
| `tree_head_not_committed` | §5.4 step 3d |
|
||
| `clip_out_of_range` | §5.4 step 3e |
|
||
| `clip_not_included` | §5.4 step 3f |
|
||
| `clip_window_invalid` | §5.4 step 3j |
|
||
| `clip_chunk_missing` | §5.4 step 3k |
|
||
|
||
`take_omitted` is a verdict; the rest are refusals. A recording verifier also raises every
|
||
code a room verifier or a take verifier raises (live-rooms §11, capture-binding §12, and the
|
||
live-events codes they list), at the room or the take; `truncated`, `malformed`,
|
||
`unknown_version`, `bad_key` and `bad_signature` for a manifest that does not read or whose
|
||
signature fails; `wrong_room` for a manifest of another room; `recording_not_permitted` for a
|
||
package of a room never recorded; `store_outside_region` for a package kept outside the room's
|
||
region pin; and `delivery_gap` and every
|
||
verdict of capture-binding §12 as a verdict. A clip verifier also raises the codes of a
|
||
descriptor that does not read (live-rooms §11); `truncated`, `malformed`, `unknown_version`,
|
||
`bad_key`, `bad_signature` and `wrong_room` for its record and its commitments; and
|
||
`commit_malformed`, `delivery_gap`, `commit_chain_broken`, `root_not_extending`,
|
||
`clock_unstable`, `capture_discontinuous`, `chunk_too_long`, `commit_early`, `commit_late`,
|
||
`chunk_reordered`, `chunk_altered` and `chunk_extra` at §5.4's steps, each with the meaning
|
||
capture-binding §12 gives it.
|
||
|
||
## 10. Conformance
|
||
|
||
[`live-conformance/recordings/`](../live-conformance/recordings/) holds the corpus, in one
|
||
file, `vectors.json`, written from fixed seeds by `wai_live_record_vectors gen`. Its
|
||
recordings are packages, each with its files and an evaluation instant and the outcome §4.5
|
||
gives them; its clips are each a room descriptor, a clip record and, per part, the device's
|
||
key, the commitments and the chunks' bytes, with the outcome §5.4 gives them.
|
||
|
||
- **The session.** A lesson room with two takes: the performer's, which is capture
|
||
binding's `lesson` take byte for byte (its manifest, chunks, receipts, consent and
|
||
commitments), and the accompanist's, captured beside it in chunks of another length on a
|
||
device whose clock runs ahead of the room's. The recording sealed by the room authority,
|
||
and by the director, stands, and so does one with a composite component, whose file
|
||
stands for a composed object and whose digest alone the verifier checks.
|
||
- **Recordings that fail a claim:** a take left out (`take_omitted`); capture binding's
|
||
`missing_chunk` take, whose chunk 6 was never uploaded (`chunk_missing`); a commitment
|
||
the relay did not deliver (`delivery_gap`); a take without its consent
|
||
(`consent_missing`).
|
||
- **The record power over the span:** a director that joins at 1 s seals the span from 1 s
|
||
and stands; sealing from 0 is refused at 0; and a director removed at 18 s, exchanged at
|
||
17 s to a grant without the power, or whose grant expires at 18 s, is refused at that
|
||
instant; one whose grant the key log revokes is refused at the room.
|
||
- **Recordings refused:** a file changed after sealing (`listing_mismatch`) and with the
|
||
listing rewritten (`component_digest_mismatch`), a take's or the composite's; a file of no component; a component
|
||
with a file §3 does not define; a manifest that breaks §4.2, of another room, or with a
|
||
bad signature; a manifest signed by a principal without the record power; a package kept
|
||
outside the room's region pin; a package of a room whose recording policy has `mode` 0; a span that
|
||
ends before a take's closing commitment; a take, and a grant, whose signature fails.
|
||
- **Good clips:** a single chunk, a byte range with trims, a whole take, the first chunk,
|
||
the last chunk, a span of the second take, a span proved against a commitment made while
|
||
the take was running, a two-take clip aligned over one window, and a span of capture
|
||
binding's `late_commit` take that its late commitment does not touch.
|
||
- **Clips refused,** one for each reason of §9 a clip raises and for each `single-take`
|
||
rule of §5.4: a proof that does not reach the head, a range past the head's size, a
|
||
span over the chunk missing from capture binding's `missing_chunk` take, a clip spliced
|
||
from two takes, a reordered, altered or extra chunk, a tree head the take never
|
||
committed, a commitment left out or malformed, another take's or another device's
|
||
commitments, a record of another room, with a bad signature, cut short, or with a trim
|
||
in an aligned clip, a window its chunks do not cover, and spans of capture binding's
|
||
`late_commit`, `commit_early`, `root_not_extending`, `clock_unstable`,
|
||
`capture_discontinuous`, `chunk_too_long` and `commit_chain_broken` takes.
|
||
|
||
A conforming verifier gives every vector the outcome, the reason, the place and the data
|
||
the corpus states. `wai_live_record_vectors verify` and the test `tests/live_record.rs`
|
||
check that the file regenerates byte for byte, that `wai-rs` gives every vector its
|
||
outcome, that every reason of §9 appears in the corpus, that §9 lists exactly the
|
||
reference's codes, and that every take the corpus says it reuses is capture binding's,
|
||
byte for byte, against `live-conformance/takes/vectors.json`.
|
||
`live-conformance/live_record_verify.py` is the second implementation, written from this
|
||
text: it shares no code with `wai-rs`; it checks range proofs by §5.3's recursion and, for
|
||
a run of one chunk, by RFC 9162 §2.1.3.2's algorithm as that RFC states it; it verifies
|
||
signatures with WAI's strict rule (`claim-conformance/_ed25519.py`), each take with the
|
||
capture-binding corpus's second implementation (`capture_take_verify.py`) and the room with
|
||
the live-rooms corpus's (`live_rooms_verify.py`); it gives every vector the same outcome;
|
||
and it mutates copies of the corpus and requires every copy to fail.
|
||
|
||
## 11. Security and privacy considerations
|
||
|
||
- The recording manifest is the signer's statement of which components the session has
|
||
and of its span; it adds nothing to a take's verdicts, which the take verifier gives from
|
||
the room's events. Its signer held the record power over the whole span, so a
|
||
participant who can only publish or watch cannot seal a recording of the room, and one
|
||
granted the power late cannot seal what came before it. The package digest names the whole;
|
||
the manifest names each component, so a component can be fetched and checked alone.
|
||
- A recording is personal data of everyone it records: the masters, and the events, whose
|
||
chats, markers and captions are principals' words. The room's region pin and
|
||
`retention_days` (live-rooms §3.7) apply to the package as they apply to the takes: the
|
||
manifest states where the package is kept, and the verifier holds it to the pin; and
|
||
a take's consents bound what `publishable` admits.
|
||
- A clip proves that its bytes are those its take's device committed while it captured,
|
||
at those indexes, under a commitment the device signed. Its trust rests on the device's
|
||
key: a verifier takes it from the device's grant or the take manifest, never from the
|
||
clip. The cutter's signature binds the cutter's choice of span, window and head and
|
||
nothing more.
|
||
- A range proof's nodes are digests of digests of other chunks' descriptors; they disclose
|
||
no byte of another chunk. A trimmed clip carries its boundary chunks whole, so the bytes
|
||
it trims travel with it; a cutter that must not disclose them cuts on chunk boundaries.
|
||
- The time a clip states is the devices' measurement of the room clock, held to *J* at
|
||
every commitment; the bytes are proved exactly, the instants to within §6.2's bound.
|
||
- A clip cut while a take runs is proved against the commitment that covers it, which the
|
||
device can no longer rewrite: any later commitment must extend it (capture-binding §4.3),
|
||
so the clip stays a span of the take as it is finally committed.
|