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-07-05 23:21:02 +02:00
2026-07-28 09:10:20 +02:00
2026-03-24 04:47:19 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00
2026-03-23 01:41:39 +02:00

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 thats 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 Apples 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 youre 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 IDEs 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 IDEs 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 IDEs toolbar or open the /iosApp directory in Xcode and run it from there.


Learn more about Kotlin Multiplatform

Description
Mantra as a kotlin multiplatform project
https://mantra.press
Readme 8.2 MiB
Languages
Kotlin 100%