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>
This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM).
-
/composeApp is for code that will be shared across your Compose Multiplatform applications. It contains several subfolders:
- commonMain is for code that’s common for all targets.
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, the iosMain folder would be the right place for such calls. Similarly, if you want to edit the Desktop (JVM) specific part, the jvmMain folder is the appropriate location.
-
/iosApp contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project.
Build and Run Android Application
To build and run the development version of the Android app, use the run configuration from the run widget in your IDE’s toolbar or build it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:assembleDebug - on Windows
.\gradlew.bat :composeApp:assembleDebug
Build and Run Desktop (JVM) Application
To build and run the development version of the desktop app, use the run configuration from the run widget in your IDE’s toolbar or run it directly from the terminal:
- on macOS/Linux
./gradlew :composeApp:run - on Windows
.\gradlew.bat :composeApp:run
Build and Run iOS Application
To build and run the development version of the iOS app, use the run configuration from the run widget in your IDE’s toolbar or open the /iosApp directory in Xcode and run it from there.
Learn more about Kotlin Multiplatform…