Files
mantra-kmp/composeApp
Kgothatso Ngako 41853ab809 feat(subgroups): the manager, the two dispatch arms, and the list that reads signatures
Phase 5 of docs/subgroups.md. `SubgroupManager` orchestrates the four steps and
reimplements none of them: step 1 is `ChillDkgRitualManager`, steps 2 and 3 are
`FrostSigningManager` sessions, step 4 is Phase 6's. What lives here is the
order, the guards, and the reading of what a parent has signed.

**There is no table of subgroups, and there should not be.** The certificates
*are* the record: the group's own signed statement, checkable without a lookup,
and held by every device that followed the signing session rather than only by
the signers -- `FrostSigningManager.complete` files a `GroupSignedEvent`
everywhere. A table beside them would be a second copy that can disagree. So
`subgroupsOf` reads the parent's kind-30329 rows, filters every one through
`certifies`, groups by child and keeps the newest.

That read has to include children this device has no room for. It is the normal
position of a parent member who is not in the subgroup, and of everybody between
the certificate being signed and the room being created -- so a list built from
rooms would be empty exactly when it is most needed. `Subgroup` carries the room
when there is one and null when there is not, and its `name` and
`adminPublicKeys` are labelled in the type as the **founding** roster: what the
parent approved, not who is in the room now.

**Two certificates for one child is a normal outcome, not a conflict.** Two
parent admins can press the button on the same admin set; the second gets the
first's ceremony back but both may still propose, and both sessions can complete.
`GroupSignedEvent` is keyed on the event id so the rows coexist, and because both
say the same true thing about the same child, which wins does not matter.
`certificateFor` takes the newest that verifies and the `d` tag makes them
replacements rather than an accumulation.

**`refuseCeremonyRoom` is where the collision from Phase 4 is caught.** A NIP-17
room's id is a pure function of its members, so one admin set gets one ceremony
room forever. Four refusals, each phrased as what to do: fewer than three admins
(the coordinator counts -- they hold a share whether or not anybody ticked them,
so asking for three *others* would quietly build a group of four); somebody who is
not in the parent; the whole group, which derives the parent's own ceremony room
and would make the "child" this very group; and an admin set that already holds a
non-failed ceremony, which is the same trap one step removed.

**Two dispatch arms, both of the sort that fail silently if forgotten.**
`ChatMessage.applyInnerEvent` gains a 30329 arm -- without it every parent member
gets a raw-JSON chat bubble per subgroup, which is exactly the failure mode
docs/member-chronicle.md reports for an old build meeting a new kind. It verifies
rather than trusts, because it is reached both from a completed session, where the
signature is checked, and from an arriving inner event, where a member could have
sent a rumor of this kind, and it cannot tell which.

Unlike the key-state arm it *does* write a line. A key state is standing state
whose session already wrote the transcript; a subgroup being born is something
that happened, and it happened on behalf of members of the parent room who are not
in the child and will otherwise never learn it exists. `TYPE_SUBGROUP_CERTIFIED`
joins the transcript's system-line dispatch and the chat-list preview, since a
type missing from either renders as a bubble -- silently, and looking exactly like
a member having said it.

`ProposedEvent` gains a 30329 summary, or the parent's admins approve "Event of
kind 30329" with the JSON underneath. It reads as the name and the member count,
which are the two things a signer can actually weigh; the id is on the screen and
their device has already checked it derives from the key the certificate names.

**30329 is deliberately not chroniclable**, with a test saying so and why. Same
shape as the key state: signed by the room, verifies perfectly, and standing
rather than work. The argument for admitting it is better -- "P certified C" is a
fixed historical fact -- and the cost of leaving it out is real, since a member
added to a parent afterwards sees an empty subgroup list. It still needs an
apply-order slot and a decision about whether a room's chronicle may carry an
event its own key did not sign, which no chroniclable kind does.

Thirteen tests in `SubgroupManagerJvmTest` over four real FROST groups, weighted
to the negative cases the way the chronicle tests are: a certificate signed by
another group, one nobody signed, and one whose id does not derive from the key it
names are each filed in the parent's room and refused on read; `record` refuses a
forgery and a certificate naming no subgroup; two certificates for one child
collapse; two children stay two, newest first; and the four refusals each fire.
397 common tests, 701 jvm tests, and `m3Audit` meets every budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:31:55 +02:00
..
2026-09-08 09:11:13 +02:00