Files
mantra-kmp/composeApp
Kgothatso Ngako 51d6a8841a feat(subgroups): a birth certificate, and the six questions that make one mean anything
Phase 1 of docs/subgroups.md. A group can now say, with a quorum, that another
group is its child -- and any device holding the event can check it without a
database, a lookup or a key it has to be told.

This is the whole of what a subgroup relationship is. A child gets its own
ChillDKG key, its own room, its own quorum and its own admins; nothing is
inherited and nothing is delegated. What the certificate carries is one
checkable claim: the group holding key P said, with a quorum, that the room C is
its child.

**Kind 30329**, past `GroupKeyStateEvent` (30326) and the chronicle pair
(30327-30328), in the same private inner-event space. Like them it says
something *about* a room rather than carrying the room's work, and like them it
only ever exists inside an encryption a relay cannot open -- so the addressable
semantics of the 3xxxx range never fire, and the `d` tag is this app's own
newest-wins rule rather than a relay's.

**Content is the child's room id, exactly as specified; the tags are what make
it checkable.** Taken literally a certificate is 32 opaque bytes, and a parent
admin would be asked to put the group's signature to a number they cannot check,
produced by a ceremony most of them were not in, on behalf of people they have
only the coordinator's word about. So the tags carry the child's threshold key,
the derivation path, its founding admins and its name. The signature covers all
of it, since an event id hashes over its tags, so nothing is added to the *claim*
by putting it there -- only to what a signer can see before agreeing. The one
that earns its place is `subgroup_key`: with it a signer's device can check
`marmotGroupId(key, path) == content` for itself, which is the difference
between approving a hash and approving a group. A coordinator who lies about who
is in the child is then lying in a field the parent's signature covers.

**`certifies` is six questions and no trust.** It is a certificate at all; it is
about this child in both the content and the `d` tag, which have to agree; it
names this parent; the child's id rederives from the key and path it carries; the
parent room signed it; and all of it inside a `runCatching`, because every input
is off the wire and a key that is not a point, a signature that is not 64 bytes
and hex that is not hex all mean the same thing here. The fourth is the half that
does not care who is speaking -- a certificate cannot be pointed at a room the
key it names did not make -- and the fifth is the half that does.

The fifth is `GroupKeyStateEvent.isSignedByRoom` used verbatim rather than
reimplemented. It already asks "did *this room* sign this", and a room id is a
public key here, which is the economy docs/member-chronicle.md is built on. Hex
is compared case-insensitively as that check compares the author, since a
certificate differing in case from what was signed fails the signature anyway --
so all this decides is whether a caller holding the same id in another case gets
a silent drop.

**What `certifies` deliberately does not check, and a test that fails if anybody
adds it.** The name and the `p` tags are the *founding* roster. A certificate is
signed once; members join and leave and rooms get renamed afterwards, and none of
that reaches a signature already made. Comparing either against a room's current
state would start rejecting valid certificates the first time somebody joined a
subgroup, and the rejection would look exactly like a forgery rather than like a
rule. `a certificate still verifies once the subgroup has been renamed and
re-staffed` is there to make that failure loud instead of subtle.

`parseAdminPublicKeys` uses `PTag.parseKey` rather than `PTag.parse`: the relay
hint a full PTag carries is not part of what the parent agreed to, and a hint
that failed to normalise would drop an admin from the roster rather than the hint
from the admin.

Two tag classes in the `FrostDerivationPathTag` shape. `SubgroupParentTag`
checks nothing beyond having a value, because what makes a parent claim mean
anything is the signature and a shape check in front of it would only decide
which of two rejections a bad value gets. `SubgroupKeyTag` borrows its shape
check from `GroupKeyStateEvent.parseThresholdPublicKey` rather than restating it,
so two readers of one value cannot disagree about what a threshold key is.

13 tests, all pure: three independent groups from `Frost.trustedDealerKeygen`, a
real FROST aggregate through the same nonce/signer-set/partial/aggregate shape
`FrostSigningManager.advance` runs, and the author and the signer pulled apart so
that both halves of question five are exercised separately. 386 common tests and
664 jvm tests pass.

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