Files
mantra-kmp/composeApp
Kgothatso Ngako 1c0acd546a feat(subgroups): put the key ceremony on both transports, p-tags and all
`ChillDkgRitualManager` ran on gift wraps only. It now runs on whichever transport
the room it is in has -- a gift-wrapped rumor in a NIP-17 room, an MLS application
message in a Marmot one -- which is the shape `FrostSigningManager` has had since
`113eda9f`, and for the same underlying reason: a group has to say something
before it owns the room the saying is about.

Nothing is wired to the new arm yet. A group's own ceremony still runs in its
NIP-17 room and a subgroup's still runs in the sibling room derived from its
admins; what changes here is that the manager stops assuming which.

**`broadcast` reads the room and writes to the matching store.** MLS state present
is what marks a room Marmot -- the same reading `sendChatMessage` makes when it
chooses between a group event and gift wraps. `replayStoredMessages` has to make
that reading again, because a ceremony's backlog is in whichever store its
messages were queued in and looking in the wrong one is a stall with nothing to
blame it on.

**The p-tags stay on both, unlike `FrostSigningManager`'s, and this is the one
thing here that would be a disaster to get wrong.** There a p-tag is an address
and a group event needs none, because the message is encrypted to the whole tree
and the signer set comes from the ceremony's host keys either way. Here the p-tag
set *is* the participant set: `acceptProposal` builds `n` out of it on every
device and `n` is hashed into the session identity, so a Marmot proposal without
them leaves every receiver unable to say what they were invited to. In a room
whose membership is wider than the ceremony they are also the only thing marking
who is in it, and a member who publishes a host key is in that group's signing
quorum for good. Inside MLS encryption, naming them leaks nothing.

They are also read off the *session* rather than off the room now, via
`participantsOf`, and sorted. A room whose membership is wider than the ceremony
is exactly the case this is for.

**`processRitualPayload` takes an `Event`.** The rumor as its sender wrote it,
which is the shape both transports hand over -- `NostrDao` already rebuilt one
from a decrypted gift wrap for the FROST arm and now does the same here. `sig` is
empty on both and nothing reads it: what vouches for the author is the seal or the
MLS frame, not a signature on the payload. `record` and `isFromCoordinator` follow.

**`proposeRitual` grows two parameters and splits its guard.** `participantPublicKeys`
defaults to the room's members, which is the whole story where the room is the
participant set; `subject` defaults to the room's name, but only for a ceremony
with no parent -- falling back there would name a child after its parent.

The guard now asks two different questions. A ceremony with no parent is scoped by
room as before. One with a parent is matched on `(room, parent, admins)` and folds
only into a ceremony that is still *running*: a completed one means that subgroup
was made, and a group is entitled to a second run by the same people. That was
impossible while the ceremony room was derived from its admins -- asking again
handed back the first ceremony's key, so the "new" subgroup was the old one under
a new name.

A new `require`, because the old invariant stops holding for free: everyone in a
ceremony has to be able to hear it. A gift wrap is sealed per p-tag so this was
true by construction; a group event reaches the MLS tree and nobody else, so a
participant outside it is an `n` that can never be met. Placed behind the
duplicate guard, so a running ceremony is still handed back whatever the room's
rows have since done.

**`NostrDao` dispatches the DKG kinds on the Marmot arm** beside the FROST ones it
already did, and `ChatMessage.applyInnerEvent` returns null for them the way it
does for `FrostSigningEvents.ALL` -- the manager writes the transcript of a
ceremony itself, and an "unsupported" row would be a second, worse account of it.
A member of the room who was not p-tagged never reaches either: their
`acceptProposal` drops the proposal and no session is opened.

**A ceremony with a parent says so in the transcript.** The line lands in a room
that already has a key, and "started a shared key ceremony. It will take 2 of 3
members to sign with the key" reads there as though *this* group were getting a
new one. It is not -- the quorum quoted is the child's, over the child's admins --
and that is the sort of misreading that gets somebody to approve a step they did
not follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:27:06 +02:00
..
2026-09-08 09:11:13 +02:00