Files
mantra-kmp/composeApp/schemas
Kgothatso Ngako 1e7ddd844a feat(subgroups): schema 19 -- the two things a ceremony's own room used to answer
A subgroup's ChillDKG is about to move out of the sibling NIP-17 room derived from
its admins and into the parent's Marmot room. That room answers two questions the
sibling room answered for free, and it answers both of them wrong.

**Who the ceremony is with.** The sibling room's membership *was* the participant
set -- it was derived from it -- so `memberPublicKeys(localChatRoom)` was the
admin list, and three callers read it that way: `broadcast`'s p-tags, the birth
certificate's `adminPublicKeys`, and the members `MarmotGroupCreation` welcomes
into the child. The parent's room is a superset, so each of those would silently
name every person the subgroup is *not*. `participantPublicKeys` is the set the
ceremony was opened on, sorted and comma-joined the way `publicShares` already
stores a list.

**What the room it makes is called.** The sibling room carried the subgroup's
name as its subject, put there by the coordinator and delivered to everyone else
by `getOrCreateNip17ChatRoom` reading the proposal's `subject` tag. A ceremony in
the parent's room has no room of its own to be named, and the parent's name is the
one name a child must not take -- `proposeBirthCertificate` normalises this string
into the certificate the parent's quorum signs, and `MarmotGroupCreation`
normalises it again onto the room. `subject` is where it lands instead.

Both are read off the proposal, which has carried both since `7bccf243` -- the
p-tags and the `subject` tag. Nothing new goes on the wire; what changes is where
it is kept. Both are nullable and both fall back to the room, which is correct for
every row written before this: a ceremony in its own NIP-17 room ran over exactly
that room's members and was named after it.

**Sorted, because two devices assemble the set differently.** The coordinator
builds it from a picker; every other device builds it from the proposal's p-tags
plus the sender. `DkgSession.formatParticipants` sorts so those are the same
string, which is what makes it something a query can match on.

Two queries, both of which exist because `DkgSession.chatRoomId` is about to stop
identifying a ceremony:

`getLatestSubgroupSessionFor(room, parent, admins)` is what "may I open another"
means for a subgroup once the parent's room hosts every one the group ever runs.
`getLatestSessionFor(room, parent)` cannot answer it -- two subgroups of one
parent share both columns -- and scoping on the room alone would refuse a group's
second subgroup on the strength of its first.

`getLatestOwnSessionForChatRoom(room)` is `getLatestSessionForChatRoom` with
`parentChatRoomId IS NULL`. It exists for `FrostSigningManager.completedKey`'s
last fallback, which is the one every parent member welcomed after the group's own
ceremony lands on: the parent's room will hold a *completed* ceremony whose key is
the child's, and a ceremony run to make a subgroup is never the room's own key.
The claim is unverified, but the only direction it can be abused in is a member
excluding a ceremony they themselves proposed, whose key they would simply not
have proposed.

`AutoMigration(18, 19)`: two nullable columns, a shape Room migrates itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:26:39 +02:00
..