Skip to content

MLS cohort group: concurrent commits from two NODES fork the epoch — resolved in-process only, admitted in the module doc, unresolved across the mesh #604

Description

@emooreatx

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:

  1. 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.
  2. 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.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions