open-standards/wai/extensions/live-worlds.md
Transaction Science 8ab46dfb02 private world: a page follows the actors' tracks before it publishes its own (#482)
privateworld failed in CI on "carol replays the world from its start". A
member's carry for an epoch is published once, when it applies the commit;
the commit that adds a page follows its join, and the clock records the join
when the page's world track appears. The page published first and followed
the clock and the actors' tracks after, so on a loaded host the carries
could go out before its subscriptions, and no wait would bring them.

A publishing page in a private world now reads the room's log to its present
end, follows every actor it records and waits for each subscription's answer,
and publishes after (E2ee.readToEnd, WorldClient.followActors). live-worlds
8.2 says a member that enters SHOULD; the e2e checks that carol followed both
members before she published.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 18:43:13 -04:00

23 KiB
Raw Permalink Blame History

WAI Extension: Live Worlds (a shared world in a room)

Status: Draft. Registers the record wai.live.world: the room authority's signed world statement, WLW1, that a room of live-rooms carries a shared world, which world its operations replay in, and the bounds every operation holds to. Each principal whose grant lets it act publishes a world track in the room: its signed operations (interactive-worlds §5.1), in op batches, one per group, each group signed into the publisher's receipt chain as a room's media track is. Every participant subscribes to every world track; one that enters late fetches each track from its first group; and each replays the merged operations under the canonical order to one session hash, whatever order they arrived in. In an end-to-end encrypted room (confidentiality) the world tracks are sealed, and each member carries the operations it holds into every epoch it reaches, so a member that joins later replays the world from its start. No new transport and no new receipt: the tracks, grants, receipts, sealing and metering are the live family's. The reference implementation is the live_world module of wai-rs (feature live_world); the corpus is live-conformance/worlds/, and its second implementation is live-conformance/live_worlds_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision: the live family this extension builds on, as merged (live-rooms, live-events, live-recording, live-metering, confidentiality), interactive-worlds, jwp-receipts and moq-streaming-format; draft-ietf-moq-transport-22 (MOQT), an Internet-Draft, a section number of which in this text is that revision's; RFC 8032 and FIPS 180-4.

1. Scope

In scope: the world statement, its signature and its rules (§3); the world track, its objects and their receipts (§4); the grant attribute that lets a principal act, and the rights it maps onto at a relay (§5); how the room's lifecycle records an actor (§6); catching up (§7); private worlds, sealed in an end-to-end encrypted room (§8); verification and the session hash (§9); metering (§10).

Out of scope: a transport; the world's simulation and its rendering, which are the world capability's (interactive-worlds §6; for wai.world.model, SPEC §5); how a principal obtains a grant.

Relationship to the core spec: wai.live.world is a record (SPEC §5): a signed record a WAI tool emits and verifies. It names no payload format and is not dispatchable. It is not the capability wai.world.live of interactive-worlds §3, whose payload is a world container with an open operation log: a room of this extension carries the operations themselves, on tracks.

2. Terms

  • Room, room authority, clock, principal, grant, grant in effect: as in live-rooms §2.
  • Op: one CausalOp (interactive-worlds §5.1), signed by its actor. Its op id is (actor, lamport).
  • Op batch: one or more ops, each whole, concatenated with nothing between them, as interactive-worlds §8 batches them.
  • Actor: a principal whose grant in effect holds the attribute world with the value act (§5). A principal's actor key is its room key: an actor signs its ops with the key its grant names.
  • World track: an actor's track of op batches (§4.1).
  • Admitted op: an op a participant has taken into its copy of the world (§9.2).
  • Private world: a world in a room whose descriptor has a confidentiality record (confidentiality §3).
  • Carry: in a private world, a member's publication of the ops it holds (§8.2).

3. The world statement

3.1 Layout

Integers are little-endian. A short field is a u8 length and that many bytes; a long field is a u16 length and that many bytes.

field size meaning
magic 4 WLW1 (ASCII)
version 1 1 in this revision
room 32 the room id (live-rooms §3.2)
capability short the capability whose ops the world tracks carry: wai.world.model in this revision
world long the world the ops replay in, in the capability's form (§3.2)
max_op_bytes 4 u32: the most bytes one op takes, from 136 to 65 536
max_ops 2 u16: the most ops one object holds, from 1 to 4 096
sig 64 §3.3

136 bytes is the size of an op with no dependency and an empty mutation (interactive-worlds §5.1).

3.2 The world

For wai.world.model, world is the 52 bytes of the model header the session hash binds (§9.3): the model's 32-byte identifier, then the generation seed as a u64, then the frame width, the frame height and the frame rate, each a u32. The width, the height and the frame rate are not 0.

3.3 Digest and signature

The digest of a statement is SHA-256 (FIPS 180-4) over its record from magic through max_ops. The signature is Ed25519 (RFC 8032) by the room descriptor's authority over the 15 bytes wai:live-world (ASCII) and 0x01, followed by the bytes the digest covers. It is 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.4 Reading and rules

A reader reads exactly the bytes it is given as one statement, in the order of §3.1, and refuses at the first of these that holds:

  1. Fewer than 4 bytes: truncated. A magic other than WLW1: malformed. No byte after the magic: truncated. A version other than 1: unknown_version.
  2. The bytes end before a field the layout requires: truncated. Bytes follow sig: malformed.
  3. capability is not wai.world.model: world_capability_unknown.
  4. world is not §3.2's form, max_op_bytes is outside 136 to 65 536, or max_ops is outside 1 to 4 096: world_statement_invalid.
  5. room is not the room id of the descriptor the statement is read beside: wrong_room.
  6. The signature does not verify under the descriptor's authority (§3.3): bad_key or bad_signature.

A room has at most one world statement, for the whole room: a room that needs another world is another room.

4. The world track

4.1 Naming

An actor P of room R publishes one world track. With R and P as 64 lowercase hexadecimal digits, its Track Namespace is [R, P, "world"] and its Track Name is 0; its receipts track (moq-streaming-format §4.1) is in the same namespace, named 0_receipts. The names keep live-rooms §6.1's rules: every byte is one of a–z, 0–9 and _, and namespace prefix matching on [R] reaches every world track of the room, as it reaches every other track of it. A world track is not a media track: it has no layers, no profile and no slot, and it is never composed. A publisher MAY list it in its catalog (live-rooms §6.2), with packaging "wai-world"; this extension reads no catalog.

4.2 Objects

Each group of a world track holds exactly one object, Object ID 0, whose payload is one op batch of at most max_ops ops, each at most max_op_bytes bytes. The groups are numbered from 0 with no gap: the actor's nth object is group n − 1. The object is not a WAI envelope, and carries no envelope flag.

In a world that is not private, every op on P's track has P as its actor. In a private world a track may also carry other actors' ops, in its carries (§8.2).

An op byte for byte the same as one already admitted, carried again on any track, is a redelivery, and changes nothing (interactive-worlds §5.3). A second op with the op id of an op already admitted and other bytes is equivocation (interactive-worlds §5.3), wherever it is carried. An op of P that P's track carries, and that is not a redelivery, has a lamport greater than that of every op of P the track carried before it, redeliveries among them (interactive-worlds §5.1's discipline, held per track).

4.3 Receipts

A world track carries receipts exactly as a room's media track does (moq-streaming-format §4.1, §5), not with the session receipts of interactive-worlds §8: each group's object is signed by P's key into a group receipt of one object (jwp-receipts §2.2, §2.3), its content hash taken over the op batch as jwp-receipts §2.1 takes it over an envelope, each group receipt chained to the group before it; the group's receipts, in the binary form of jwp-receipts §2.7.3, are the receipts track's object of the same group, sent no later than the object; and the object carries its WAI_RECEIPT property, except where it is sealed (§8.1; confidentiality §4.3). Its receipts' track_alias (jwp-receipts §2.4.1) is 4, the world track's receipt alias: an actor's catalog that lists the track states it as wai_receipt_alias, and gives no other of the actor's tracks that alias. The figure each receipt carries is the publisher's energy figure for the object, under its acquisition class (energy-measurement §2), or the unmetered marker where it measured none: a reader shows the marker as unmetered, never as a figure of zero joules.

A relay that checks receipts at a door holds every world track to its owner's key: a door rule over ["*", "*", "world"] whose trusted field is the namespace's second (live-rooms §6.1). The receipts bind the batch to the track and its position; each op's own signature binds it to its actor. A participant checks both (§9).

5. Rights

5.1 The attribute world

A grant lets its principal act in the room's world when its attributes (live-rooms §4.4) hold the key world with the value act. A grant without the key does not; one whose world holds another value does not either. A grant that may subscribe (live-rooms §3.3) reads every world track of the room, as it reads every track of the room. An actor's grant need publish no media kind: acting is not publishing media.

5.2 At a relay

A grant whose world is act maps onto one scope beyond live-rooms §10.2's, after its step 4: [[2, 6], [R, P, "world", nil]] (PUBLISH_NAMESPACE and PUBLISH of its world track and the track's receipts track). A relay that honours a room's grants (live-rooms §10.5) accepts a PUBLISH under [R, P, "world"] only from a session whose SETUP proved such a grant for P in R, and refuses any other with REQUEST_ERROR UNAUTHORIZED (MOQT §12.3): a principal never publishes another's world track, and a grant without the attribute publishes none.

5.3 Seeking back

Catching up (§7) fetches a world track below its live edge, which live-rooms §10.6 serves only to a grant that holds the seek power. A room authority that admits actors SHOULD give every grant of a world room the seek power.

6. The lifecycle

An actor is recorded in the room's lifecycle (live-rooms §7.2) by the room's clock, as any principal is: the clock signs its join, naming its grant, when its world track appears at the relay, and its leave when the track ends. Its world track is not a media track, so the clock signs no publish for it. A participant learns the room's actors from the clock's chain (live-events §8.1): every principal whose join names a grant holding the attribute, present or since gone. A page authors none of the lifecycle.

7. Catching up

A participant that enters a room in progress, for each actor §6 names, fetches the actor's world track and its receipts track from group 0 to the live edge (MOQT §3.2; live-rooms §10.6), and admits the fetched objects in group order before any object of that track it received live. An object received both ways is admitted once. It does so for an actor that has left as well: its ops are part of the world.

A relay that carries a world room SHOULD keep every group of the room's world tracks and of their receipts tracks, from group 0, while the room lasts: for example a DVR window (live-recording §8) over ["*", "*", "world"], which the ops' small size makes cheap. Because a track's groups are numbered from 0 with no gap (§4.2), a participant knows when its copy of a track is whole: a fetch that does not return every group from 0 to the live edge leaves the world incomplete, world_incomplete, at that track and its first missing group. A participant whose world is incomplete MUST NOT report a session hash as the room's; it MAY show the world it holds, marked incomplete.

8. Private worlds

8.1 Sealing

In a private world every object of every world track is sealed as confidentiality §4 seals the object of a media track, under the actor's keys of its current epoch (confidentiality §5.7), and opened as confidentiality §6 opens one: this extension adds the world track to the tracks confidentiality §1 seals. The original payload (confidentiality §4.4) is the op batch. The receipt is over the sealed bytes (confidentiality §8.1), so a relay routes, caches, receipts and meters ciphertext alone. An actor's grant may subscribe, so its principal is a member once the clock records its join (confidentiality §5.4); a participant outside the room's group opens nothing. Every world track remains its owner's: confidentiality §6 step 6 refuses an object sealed under another member's Key ID.

8.2 Carries

A member opens only what is sealed in the epochs it holds, from the epoch that added it (confidentiality §5.7), and a relay keeps nothing of such a room (confidentiality §9). Fetching cannot bring a later member the ops sealed before it joined. Members carry them instead:

  • A member MUST, in each epoch it reaches, its first included, before any other object it seals in that epoch, publish a carry: every op it has admitted, of every actor, the actors in ascending bytewise order of their keys and each actor's ops in ascending lamport, in as many objects of its own world track as max_ops requires, each sealed in that epoch. The order keeps §4.2's rule for the member's own ops, which a later member may not have held.
  • Until it holds an epoch, a member publishes nothing; an op it makes meanwhile waits, and is published after its first carry.
  • A carry is published once. A member that enters SHOULD follow the world track of every actor the room's log records before it publishes its own: its own track is what makes the clock record its join (§6), and so brings the epoch the members carry in. A carry published before the member followed its sender's track does not reach it.
  • A participant admits each op of a carry as it admits any op (§9.2). A carried op of another actor is that actor's, by its own signature: a carry cannot make an op, and an op it carries again is a redelivery.

A carry can leave out an op, and no participant can tell from the carry alone (§12).

9. Verification

9.1 Inputs

A verifier is given: the room's descriptor and grants, which it checks as live-rooms §4.3 does; the world statement; whether the world is private; and, for each world track, its owner and its objects in group order, each with its Group ID, its receipts-track object and the bytes its receipt covers, and, in a private world, the op batch that opened from them (opening is confidentiality §6's, and its corpus is confidentiality's). Each track's objects are what the verifier received of it, live and fetched, each location once.

9.2 Steps

The verifier reads the descriptor (live-rooms §3.4), checks the grants in order (live-rooms §4.3) and reads the statement (§3.4), and refuses at the first failure, naming the descriptor, the grant or the statement. It then takes the tracks in the order given, and each object of a track in group order through every step below before the next, and refuses at the first of these that fails, naming the track and the group:

  1. The owner has a grant in the list whose world is act (§5.1): world_not_granted.
  2. The track's first object is group 0, and each later object is the group after the one before it: world_gap. In a private world the first object is the first the verifier opened, at any group, since a member opens nothing sealed before it joined (§8.2); each later object is still the group after the one before it.
  3. The receipts-track object reads (jwp-receipts §2.7.4) and holds one group receipt and one object receipt of that group: malformed. Its group receipt is signed by the owner's key, with track_alias 4 (§4.3), and both receipts name the object's Group ID, the object receipt Object ID 0: otherwise moqt_mismatch (moq-streaming-format §4.1), before any signature is checked. The receipts then verify by jwp-receipts §3 against the bytes the receipt covers: the group's signature, the content hash, the object's proof to the signed root, the group's total (the object's figure is the group's, and a labelled group's class is the one its object composes to), and the chain, each group receipt's parent the link hash of the group receipt before it on the track and the first's none. In a private world the first object the verifier opened, when it is above group 0, names the group before it, which the verifier does not hold: its parent is not checked, and it starts the chain. The reason jwp-receipts §3.3 names is reported.
  4. The op batch is one or more ops, each whole (interactive-worlds §5.1): op_malformed. It holds at most max_ops ops: batch_too_large. Each op is at most max_op_bytes bytes: op_too_large.
  5. Each op, in batch order: its signature verifies under its actor (interactive-worlds §5.1, SPEC §7 Signatures): bad_op_signature; its actor is the owner, in a world that is not private: actor_not_owner; it is an op of the statement's capability (for wai.world.model, an action, mutation kind 1, whose body reads): op_not_action; it is not equivocation (§4.2): equivocation; where its actor is the owner and it is not a redelivery, it keeps §4.2's Lamport rule: lamport_regress.

An op that passes is admitted; a redelivery was admitted already, and is not admitted again.

9.3 The session hash

The room's world is the set of admitted ops, each once. For wai.world.model, its session hash is the action trace's (SPEC §5): BLAKE3 over the 24 bytes wai:world-model-session (ASCII) and 0x01, followed by the statement's world, chained through each op in canonical order (interactive-worlds §5.2): the chain's next value is BLAKE3 over that domain, the value before it and the op's whole bytes, its signature included. Two participants that hold the same admitted ops hold the same session hash, whatever order the ops arrived in and whichever tracks carried them.

9.4 Outcomes

A verification that refuses states the reason and where. One that passes states the session hash, the number of admitted ops and the actors whose ops it admitted, each with the number of its ops; and, for a participant that caught up, whether its world is whole (§7).

10. Metering

A relay's per-hop session record (live-metering §3) counts a room's world tracks with its other tracks: neither media tracks nor events tracks, they are counted together under the record's entry for the room's other tracks. A world adds no record of its own.

11. Reasons

The codes this extension adds, each with where it is raised:

reason raised at
world_capability_unknown §3.4 step 3
world_statement_invalid §3.4 step 4
world_not_granted §5.1, §9.2 step 1
world_gap §4.2, §9.2 step 2
world_incomplete §7
op_malformed §9.2 step 4
batch_too_large §4.2, §9.2 step 4
op_too_large §4.2, §9.2 step 4
bad_op_signature §9.2 step 5
actor_not_owner §4.2, §9.2 step 5
op_not_action §9.2 step 5
lamport_regress §4.2, §9.2 step 5
equivocation §4.2, §9.2 step 5

Of live-events §9's codes, a verifier also raises truncated, malformed and unknown_version when a statement does not read, bad_key and bad_signature for its signature, and wrong_room for a statement of another room (§3.4). Of jwp-receipts §3.3's, it raises moqt_mismatch and those its §3 raises for a receipt, chain_break and parent_not_held among them (§9.2 step 3). world_incomplete is a participant's report about its own copy, not a refusal of the room.

12. Conformance

live-conformance/worlds/ holds the corpus, in one file, vectors.json. Each vector gives the descriptor, the grants, the statement, whether the world is private, and the world tracks, and either the outcome (§9.4) or the refusal, its reason and where. wai-rs's wai_live_world_vectors gen writes it from fixed seeds, byte for byte the same each time, and check verifies it; live-conformance/live_worlds_verify.py, written from this text, verifies it too, and the two agree on every vector. The corpus holds, as negative controls, every code §11 adds that a verifier raises, the codes of §3.4 a statement can reach (bad_key cannot be: a statement's key is the descriptor's authority, which the descriptor's own signature held to SPEC §7 already), and of jwp-receipts §3.3 malformed, moqt_mismatch, signature_verify_failed, content_hash_mismatch, group_total_mismatch, chain_break and parent_not_held; and accepted worlds, public, and private with carries that bring a late member the ops sealed before it joined.

A browser participant is the reference room client's world page (joule-moq-web, web/world/), which runs §9.2 in the page through wai-live-web and checks receipts as the room client does. The room service joule-moq-room keeps world rooms ([world]), and its tests and the world and privateworld scenarios of joule-moq-web/e2e/ run two participants and a late one through joule-moq-relay.

13. Security and privacy considerations

  • What a receipt proves, and what an op's signature proves. A world track's receipts prove that its owner published each batch at its position in its chain, and the door that checked them; an op's signature proves its actor made it. Neither proves the op was sensible: a world's simulation decides what an op does (interactive-worlds §6), and every participant decides it the same way.
  • Withholding. A relay can withhold groups of a world track, as it can any track's; a participant whose fetch has a gap knows its world is incomplete (§7), and two participants that compare session hashes see that they differ. In a private world a member can leave ops out of its carry, which a later member cannot detect from that carry; it can compare its session hash with another member's, and the other members carry what they hold too.
  • Equivocation. An actor that signs two ops with one op id is refused at the second wherever it arrives (§4.2), so it cannot show two participants two different worlds under one history without being caught by whichever receives both.
  • Flooding. max_op_bytes and max_ops bound an object; a relay's per-session limits bound a session's rate; the room authority bounds the actors by the grants it issues.
  • Privacy. A public world's ops, actors and timing are visible to every participant and every relay. A private world's ops are visible to its members alone; its relays see the ciphertext's size and timing, the track names (and so which principals act) and the receipts, as for any sealed track (confidentiality §8.3).