docs(cordn): record the real reason multi-device is a non-goal

Both plans said there was "nothing interoperable to implement." That is
true of exactly one field and false of the feature, and the difference
decides what a future revisit should be looking for.

A group document's `clientState` is library-private on purpose —
multi-device.md §4.2 says it is "library-serialized and intentionally not
pinned to a wire format", with TLS pinned only for
`lastResortKeyPackage`. So cross-client sync is ruled out by design and
no work on our side changes it. But everything §14 lists as a MUST is
vendor-neutral: the document shapes, the NIP-44 v2 DEK seal, sha256
addressing, highest-epoch-wins reconciliation, the {gid, epoch}
tombstone, the sibling-skip rule, the tip format. An Amethyst-only fleet
is buildable with MlsGroupState in that one field.

It stays a non-goal on cost, now written down as such: it would ship
§10's symmetric commit race, which the spec leaves unresolved (§15:
"equal-epoch MLS states have no merge function"; automatic resolution is
"possible but unspecified"), behind an always-on discipline — republish
plus full tip rewrite after every epoch-advancing Commit, full reconcile
before opening any delivery stream — on a Draft spec, for a feature no
mixed-client user benefits from.

Each plan now names the two independent conditions that would reopen it:
a library-neutral clientState, or a specified equal-epoch tiebreaker.

Also corrected the backup section's cross-reference, which borrowed §4.6's
reasoning: it applies to cross-client portability specifically, and the
fork-on-restore hazard is the same equal-epoch divergence, reached by
restore rather than by a race.

Docs only; no code touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
This commit is contained in:
Claude
2026-09-23 00:33:12 +00:00
parent fd392b4f9a
commit bd4e597ef4
2 changed files with 76 additions and 15 deletions
+35 -8
View File
@@ -391,13 +391,38 @@ client-side from Commits and never transmits them (synthetic kind `-1`). Derivin
better here — it cannot disagree with the MLS state it describes, and it costs no bytes — and it
is what a cordn peer will do anyway. Take theirs for cordn. Do not change Marmot.
### 5.3 Multi-device stays a non-goal
### 5.3 Multi-device stays a non-goal — but not for the reason first recorded
`spec/applications/multi-device.md` ships `base64(serialized ts-mls ClientState)` inside sealed
documents. That is **their library's internal state**, not a wire format — nothing but ts-mls can
produce or consume it, so there is nothing interoperable to implement. The interop plan already
records this (§4.6) and it should stay recorded rather than quietly reappearing as a settings
screen. If upstream ever specifies a library-neutral device-sync format, revisit.
The original wording here said there was "nothing interoperable to implement." That is true of
exactly one field and false of the feature. Corrected, because the distinction decides what a
future revisit would even be looking for:
- **Cross-client sync is impossible by design.** A group document's `clientState` is
`base64(serialized MLS ClientState)`, and `multi-device.md` §4.2 states that it is
*"library-serialized and intentionally not pinned to a wire format."* TLS is pinned for
`lastResortKeyPackage` and nothing else. So an Amethyst device and a cordn-web device can never
share a leaf, no matter what we build.
- **Our own fleet is buildable.** Everything §14 lists as a MUST is vendor-neutral: the document
JSON shapes, the NIP-44 v2 seal to a per-identity DEK, `sha256` addressing, highest-epoch-wins
reconciliation, the `{gid, epoch}` tombstone, the sibling-skip rule, the tip format. Only that
one field is ours to choose, and `MlsGroupState` fits it.
It stays a non-goal on cost, which is the part worth re-reading on a revisit:
- Amethyst-only device sync. A user who mixes clients gets nothing from it.
- §10's **symmetric commit race has no resolution in the spec**. Two devices committing inside one
delivery round-trip both land on epoch N+1 with different states; the forward-only epoch check
cannot break a tie, and §15 concedes *"equal-epoch MLS states have no merge function."* The
spec's answer is refuse-to-commit-while-behind plus a manual re-sync prompt. Shipping this means
shipping that race and a conflict UI for it.
- The discipline is always-on: a document republish and a full tip rewrite after **every**
epoch-advancing Commit (§10.5), and a full reconcile before opening any delivery stream (§10.6).
- The spec is still Draft.
So it should stay recorded rather than quietly reappearing as a settings screen. Revisit if
upstream pins a library-neutral `clientState` (which would make it cross-client and change the
first bullet) **or** specifies the equal-epoch tiebreaker (which would remove the race). Either
one alone moves the decision; neither has happened.
### 5.4 Which backup story — DECIDED: our own format, and LANDED
@@ -409,7 +434,7 @@ file will not import). Needs a call; no strong opinion here.
- *Portability was never available.* The valuable content of any cordn backup is MLS group
state, which is an engine's internal serialization rather than a wire format — the same
reasoning §4.6 of the interop plan uses to rule out multi-device. Theirs is ts-mls's
reasoning §4.6 of the interop plan uses to rule out *cross-client* multi-device. Theirs is ts-mls's
`ClientState`, ours is `MlsGroupState`, and neither reads the other whatever container wraps
it. A byte-compatible file would buy a restore that cannot restore. Their client also sits
outside the two MIT packages, and the spec prose is unlicensed.
@@ -419,7 +444,9 @@ file will not import). Needs a call; no strong opinion here.
So: `CordnBackup` — our own versioned format, scrypt at NIP-49's cost plus ChaCha20-Poly1305.
Restoring **replaces** a device rather than merging, because an MLS state export is a cloneable
identity and two devices committing from one state fork the ratchet tree (§5.3 again).
identity and two devices committing from one state fork the ratchet tree — the same equal-epoch
divergence §5.3 records as unresolved in `multi-device.md` §10, reached here by restore instead of
by a race.
## 6. Out of scope, and why
+41 -7
View File
@@ -215,11 +215,41 @@ the file key with `aad = mime‖0x00‖filename‖0x00‖sha256(plaintext)`, whe
`HKDF-Expand(exporter, context)` with `aad = "mip04-v2"‖0x00‖hash‖0x00‖mime‖0x00‖filename`.
Separate codec; shared primitives (ChaCha20-Poly1305, NIP-92 `imeta`, Blossom) all already exist.
### 4.6 Multi-device is not interoperable — explicit non-goal
### 4.6 Multi-device — cross-client interop is impossible; the feature is not
`spec/applications/multi-device.md` ships `base64(serialized ts-mls ClientState)` inside sealed
Blossom documents advertised by an opaque tip. That is a ts-mls-internal serialization, not an MLS
wire format. There is nothing to implement against. Do not attempt it.
`spec/applications/multi-device.md` gives a user's devices **one shared MLS leaf** per group and
carries that group's `ClientState` inside sealed Blossom documents advertised by an opaque tip.
**What is not implementable: interop with their devices.** The document's `clientState` is
`base64(serialized MLS ClientState)`, and §4.2 says outright that it is *"library-serialized and
intentionally not pinned to a wire format"* — the only pinned MLS serialization in the document is
TLS, used for `lastResortKeyPackage`. So the field is ts-mls's private encoding by design, not by
omission. An Amethyst device and a cordn-web device can never share a leaf, and no work on our side
changes that.
**What IS implementable: our own fleet.** Everything else §14 lists as a MUST is pinned and
vendor-neutral — the two document JSON shapes, the NIP-44 v2 seal to a per-identity DEK, `sha256`
content addressing, the highest-epoch-wins reconciliation rule, the `{gid, epoch}` tombstone shape,
the sibling-skip rule, and the tip format. Amethyst-device ↔ Amethyst-device sync would work with
`MlsGroupState` in that one field.
**Why it stays a non-goal anyway**, which is the honest reason and not the one above:
- It buys Amethyst-only device sync. A user who mixes clients gets nothing.
- §10's **symmetric commit race is unresolved in the spec**. Two devices committing inside one
delivery round-trip both reach epoch N+1 with different states; the forward-only epoch check
cannot break the tie, and §15 concedes that *"equal-epoch MLS states have no merge function."*
The spec offers only refuse-to-commit-while-behind and a manual re-sync prompt; automatic
resolution is *"possible but unspecified."* We would be shipping that race.
- It is a heavy, always-on discipline for a Draft spec: a document republish plus a full tip
rewrite after **every** epoch-advancing Commit (§10.5), and a full reconcile before opening any
delivery stream on startup (§10.6), because a backlog fetched while behind arrives sealed under
epochs the device has not adopted.
- It needs an `MlsGroupState` ⇄ document codec and a `prev`-chain walk (§8.5) that nothing else
in the codebase wants.
So: do not attempt it — but record it as a cost/benefit call on a Draft spec with a known race,
not as "there is nothing to implement."
## 5. What we already have
@@ -1155,8 +1185,10 @@ Still open in Stage 3:
- **A ts-mls `ClientState` export.** `verify.ts` carries an optional gate that
decodes a Kotlin-exported ts-mls state and sends from it. We write no such
file, so it is skipped. Producing one means re-encoding `MlsGroupState` into
ts-mls's layout — real work, and only needed for multi-device, an explicit
non-goal (§4.6).
ts-mls's layout — real work, and the only thing it would buy is cross-client
multi-device, which §4.6 shows the spec rules out by design (`clientState` is
deliberately library-private). So this gate has no reachable purpose, not
merely a deferred one.
- **Tier B**, live against `ghcr.io/cordn-msg/cordn:latest`.
### Stage 4 — App integration — LANDED (disclosure UI + headless layer); group UI open
@@ -1255,7 +1287,9 @@ The original scope, for reference:
### Non-goals
- Multi-device (§4.6) — nothing interoperable to build.
- Multi-device (§4.6). Cross-client sync is impossible by design — `clientState` is
deliberately library-private — and our own fleet is buildable but not worth it: it would
ship §10's unresolved equal-epoch commit race, for an Amethyst-only feature, on a Draft spec.
- Reusing any `Marmot*`/`Mip*` type for cordn.
- A production Kotlin ContextVM *server*. The Tier C fixture (§6.4) plays the server role for
tests only. For a real coordinator, `cordn-rs` exists, is faster, and shares the SQLite schema —