Surfaced while grounding CIRISPersist#848 in the Constitution. Edge's own group-key layer has the same shape of gap as the one #848 is being asked to close, one layer up.
What the code says about itself
src/mls/cohort_group.rs, module doc:
"Single writer per community_id. Two concurrent commits against the same group fork the epoch (both committers believe they own N+1; one of them is wrong and its peers will reject its subsequent messages). Every mutating method here takes an async mutex, and CohortGroups hands out clones of the same CohortGroup handle per community_id so two independently-obtained handles share one lock rather than racing two in-memory copies of the same group."
and on the registry:
"CohortGroup's mutex only enforces single-writer for holders of the same handle… The registry closes that: open is get-or-create, so every caller naming a community_id gets a clone of one handle."
The witness, concurrent_commits_serialize_instead_of_forking_the_epoch, spawns eight rotate()s through clones of one handle in one process and asserts contiguous epochs. It proves the mutex. It cannot reach the case the doc names, because the doc names a cross-node problem and the fix is a process-local lock.
Why it has not bitten
Rooms are two-party with both founders moderators, and the mesh harness runs K=1. Two founders on two nodes committing (an Add, a Remove, or a hygiene rotate) inside one replication round has simply not happened in any run that would notice. When it does, one node's MLS state is on a branch the other rejects, and every subsequent message from the loser is refused — with no signal beyond WrongEpoch on the receiving side.
What the Constitution requires
This is the same ruling that governs CIRISPersist#848:
- Part 3, composition: "There is no compare-and-swap across a mesh, so concurrent claims are expected rather than prevented and MUST settle on earliest
claimed_at, ties broken on the lowest occurrence key_id."
- Ledger clause 3: "A ledger MUST declare its serialization discipline — a write lease, or per-delegate sub-ledgers folded deterministically. Concurrent unleased writes at one sequence number ARE a fork, by definition."
- CC 5.1: rekey "conforms to MLS TreeKEM (RFC 9420, normative)" — but RFC 9420 delegates commit ordering to a Delivery Service, and the CC has none by design ("Total order is its own kind of ruin", Foreword). CC 5.4.5's per-community witness chain, "signed inside the community's MLS encryption," is the in-group surface the CC provides for detecting and anchoring exactly this.
So the room needs a declared discipline. The MLS-shaped options:
- A committer lease settled by the convergent-merge claim — earliest
claimed_at, lowest occurrence key_id — over the community rows the handshake already rides. Coordination-free; the loser sees the winner's commit, discards its pending commit, and re-proposes. This is "MLS without a DS" done the CC's way rather than DCGKA's.
- Proposal-only from non-leaseholders: members emit MLS Proposals as community rows; only the leaseholder commits. RFC 9420 supports this natively and it removes the race rather than resolving it.
- Accept the fork and reconcile with a deterministic rule anchored on the witness chain (CC 5.4.5) — the SSB shape. Correct, and the most surface.
(2) is the smallest and the most RFC-native; (1) is what #848 will likely need anyway, so sharing the claim primitive between the two would be one discipline declared once.
The shared decision
#848 and this issue are the same question asked of two key layers that today live in two repos (agreement in edge, the content cascade in persist) and must agree on an epoch. The CC's steady state — content keys derived from the MLS exporter (CC 5.4), agreement amortized into TreeKEM — assumes they are one substrate. Whatever discipline is chosen should be chosen once, and the mesh harness should grow a leg where two founders commit inside one round, which is the run that would have found this.
Refs CIRISPersist#848, CIRISEdge#601.
Surfaced while grounding CIRISPersist#848 in the Constitution. Edge's own group-key layer has the same shape of gap as the one #848 is being asked to close, one layer up.
What the code says about itself
src/mls/cohort_group.rs, module doc:and on the registry:
The witness,
concurrent_commits_serialize_instead_of_forking_the_epoch, spawns eightrotate()s through clones of one handle in one process and asserts contiguous epochs. It proves the mutex. It cannot reach the case the doc names, because the doc names a cross-node problem and the fix is a process-local lock.Why it has not bitten
Rooms are two-party with both founders moderators, and the mesh harness runs K=1. Two founders on two nodes committing (an Add, a Remove, or a hygiene rotate) inside one replication round has simply not happened in any run that would notice. When it does, one node's MLS state is on a branch the other rejects, and every subsequent message from the loser is refused — with no signal beyond
WrongEpochon the receiving side.What the Constitution requires
This is the same ruling that governs CIRISPersist#848:
claimed_at, ties broken on the lowest occurrencekey_id."So the room needs a declared discipline. The MLS-shaped options:
claimed_at, lowest occurrencekey_id— over the community rows the handshake already rides. Coordination-free; the loser sees the winner's commit, discards its pending commit, and re-proposes. This is "MLS without a DS" done the CC's way rather than DCGKA's.(2) is the smallest and the most RFC-native; (1) is what #848 will likely need anyway, so sharing the claim primitive between the two would be one discipline declared once.
The shared decision
#848 and this issue are the same question asked of two key layers that today live in two repos (agreement in edge, the content cascade in persist) and must agree on an epoch. The CC's steady state — content keys derived from the MLS exporter (CC 5.4), agreement amortized into TreeKEM — assumes they are one substrate. Whatever discipline is chosen should be chosen once, and the mesh harness should grow a leg where two founders commit inside one round, which is the run that would have found this.
Refs CIRISPersist#848, CIRISEdge#601.