9c50b4fceee56f7486766ace2311c19a689e4548
1 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3995cdefc6 |
feat: run a ChillDKG ritual over a NIP-17 group
Robust groups can now generate a FROST threshold key together. The
group's members are the participants, the room's creator is the
coordinator, and the whole protocol travels as gift-wrapped rumors on
the chat the group already has -- so there is no second transport to
build, operate or debug.
This is what the quorum has been reaching for since it was introduced.
Until now "t of n must approve" had no key to approve anything with;
ChillDKG produces one that no single member holds.
## Transport: seven rumor kinds (nostr/dkg/)
coordinator --[ 30310 proposal ]-> everyone
participant --[ 30311 host key ]-> everyone
participant --[ 30312 round 1 ]-> everyone ParticipantMsg1
coordinator --[ 30313 coord round 1 ]-> everyone CoordinatorMsg1
participant --[ 30314 round 2 ]-> everyone ParticipantMsg2
coordinator --[ 30315 certificate ]-> everyone CoordinatorMsg2
anyone --[ 30316 failure ]-> everyone abort + reason
These only ever exist inside a NIP-17 gift wrap, so no relay sees them
unencrypted and the replaceable semantics normally implied by the 3xxxx
range never apply -- which is why they can sit next to the app's other
private kinds (30300-30309) without meaning anything different.
Every message is addressed to the whole group, even the two the protocol
only needs the coordinator to read. NIP-17 wraps per recipient anyway,
ChillDKG treats the coordinator as untrusted by construction, and having
every member observe the ritual is what makes a progress UI possible
without a side channel.
`DkgSessionIdTag` is on every message: a group may abandon an attempt and
start another, and a straggler from the dead one must be dropped rather
than mixed into the live session. `DkgThresholdTag` rides the proposal so
every participant validates the same SessionParams -- disagreement on `t`
fails the session instead of quietly producing a weaker key.
## Persistence: inputs, not state (database/model/Dkg*, schema v2)
DkgSession deliberately stores no protocol state. Reading EncPedPop
confirms randomness enters the participant steps only through the passed
`random`/`auxRand` arguments (`simplSeed = taggedHash("encpedpop seed",
seed + random + encContext)`), so every ChillDkg step is a pure function
of inputs. Keeping the two 32-byte randoms plus the received messages is
therefore enough to recompute any intermediate state on demand, and the
opaque ParticipantState/CoordinatorState objects -- which have no
serialization API -- never need to be persisted at all.
That is not a micro-optimisation. A DKG cannot finish unless all n
members take part, and chat users close apps mid-round; recomputation is
what lets a ritual resume instead of forcing the group to start over.
DkgParticipantMessage is keyed (sessionId, participantPublicKey, kind) so
a redelivered message overwrites rather than accumulates -- relays
redeliver, and a duplicated round-1 message would hand the coordinator a
participant list of the wrong length.
Database goes to version 2 with an AutoMigration: v2 only adds tables, so
Room generates it. Schema 2.json is exported alongside.
## Driving it (managers/ChillDkgRitualManager.kt)
State machine driven entirely by arriving messages: persist, then ask
whether the ritual can move. Because every step is recomputable there is
no long-lived session in memory to lose, and processing is idempotent --
a redelivered message re-runs a step that has already been taken and
changes nothing.
The coordinator is a participant too, so it records its own outbound
messages locally: its round-1 message has to be in its own aggregation
alongside everyone else's. Being the room's creator buys it no authority
here -- ChillDKG's coordinator relays but cannot learn secrets or bias
the key -- only the job of aggregating.
Two decisions worth knowing:
* Host keys are DERIVED, not reused. `sha256("mantra/chilldkg/host-key/v1"
|| nostrSeckey)`. Reusing the nostr identity key directly was the
simpler option, but one secret serving two protocols means a flaw in
either reaches the other. Deriving from the same seed keeps it
recoverable from the wallet backup, which matters because ChillDKG
needs the host secret key to recover a session's outputs and asking
chat users to back up a second secret is how keys get lost.
* Participant order is a bytewise sort of the host public keys. ChillDKG
fails outright if participants disagree on ordering, and a sort is the
only order every device can derive independently from the same set.
Any ChillDkg exception ends the session for this device and is broadcast
as a 30316 so the rest of the group stops waiting, rather than leaving
every member on a spinner that will never resolve.
## Inbound (database/dao/NostrDao.kt)
One branch on the existing decrypted-gift-wrap dispatch, beside the
kind-14 and WelcomeEvent branches, handing ritual kinds to the manager.
## UI (ui/.../DkgRitualScreen.kt + view model, state, route)
Reached from chat room detail via "Shared Key", shown only for rooms with
no MLS state -- i.e. the NIP-17/robust ones. An MLS room has a single
admin and no group key to share, so the entry point would be a lie there.
The screen is a ladder of rounds with real counts ("3 of 5") rather than
a spinner. The unusual thing about a DKG, and the thing the UI has to get
across, is that it needs *everyone* at once; a count says who it is
waiting on, an indeterminate spinner says nothing. The coordinator gets
the start button, everyone else is told who they are waiting for, and a
failed ritual states plainly that no key was created and it is safe to
run again.
DkgSession.threshold finally gives the quorum somewhere to live. The
value chosen during group creation is still not persisted on ChatRoom,
so this screen re-asks with the same majority default rather than
inventing a different one; there is a TODO where that gap closes.
Verified:
./gradlew :composeApp:compileCommonMainKotlinMetadata
./gradlew :composeApp:compileDebugKotlinAndroid
Not runtime-verified: exercising a DKG needs several devices exchanging
live messages, and the library's own vector suite needs JDK 21.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|