Commit Graph

60 Commits

Author SHA1 Message Date
Kgothatso Ngako
296011dcd5 feat: give a queued message somewhere to say who it is private to
Two nullable columns and the v5 migration that adds them, ahead of the
code that fills them, so the schema lands on its own and can be reverted
on its own.

MarmotInnerEvent.directMessageRecipientPublicKey is the outbound signal.
The notary reads a queued row and has no other way to know a message is
meant for one member rather than the room -- the plaintext is identical
either way -- so this is what routes it into the gift wrap path. Inbound
rows leave it null on purpose: the recipient is on the wrap's `p` tag,
which is where every member reads it from, so a second copy on the row
would be a second thing that can disagree.

ChatMessage.directMessageRecipientPublicKey is what the transcript reads.
Both lines a direct message can produce need it -- the one its two parties
see, and the "sent a private message to Bob" line everybody else gets --
and holding it on the row keeps the view model off a join for a fact it
already has to render.

MarmotInnerEvent hand-writes equals and hashCode over every field, so both
are extended too. A field missing from those is not a compile error and
not a test failure; it is two rows that differ comparing equal, which
surfaces much later as an upsert that does nothing.

Room generates the migration -- verified as two ADD COLUMNs with no table
rebuild, so nothing is copied and nothing can be dropped:

  ALTER TABLE `ChatMessage` ADD COLUMN `directMessageRecipientPublicKey` TEXT DEFAULT NULL
  ALTER TABLE `MarmotInnerEvent` ADD COLUMN `directMessageRecipientPublicKey` TEXT DEFAULT NULL

Rows written before this come back null, which reads as "not a direct
message" -- the only answer that is true of all of them.

v5 is an AutoMigration entry rather than a hand-written Migration like
MIGRATION_3_4 next to it, because that one rewrote data without changing
shape and this one changes shape without touching data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 18:58:57 +02:00
Kgothatso Ngako
cae50ce359 feat: hold the ritual until its owner approves each step
The ChillDKG ritual ran entirely on its own. `acceptProposal` published this
device's host key the moment a PROPOSAL arrived from a relay, and `advance`
published rounds 1 and 2 as soon as their inputs landed. Receiving a nostr event
was therefore enough to enrol the owner of a phone in a group's permanent signing
quorum, without anything having been shown to them first.

Nothing of this device's own now goes out before its owner says so. Three
approvals, because each publishes something different and commits the member to
something different:

  host key  joins the ceremony, and fixes n. A member who joins and then stops
            answering does not merely fail to help -- the ritual cannot finish
            without every member, so they hold it open for everybody.
  round 1   contributes to the key itself. The member's own secret material
            starts shaping a key they will be expected to help sign with.
  round 2   confirms the coordinator's combined result matches what this device
            sent. A check rather than a formality: it is what stops a coordinator
            substituting a key the members never contributed to.

The coordinator's two aggregations are deliberately not gated. They relay other
members' already-published messages and disclose nothing of the coordinator's own,
so an approval there would stall the whole group on one person's attention without
protecting anybody. The member who opens a ceremony is auto-approved for the host
key alone -- starting one is already the act of agreeing to be in it -- and is
still asked for rounds 1 and 2, which publish key material.

Each gate returns rather than throwing. The ritual is not failing, it is waiting
on a person; everything already received stays stored, so it resumes the moment
they approve. `pendingApproval` mirrors those gates exactly and has to keep doing
so: if the two disagree the screen offers an approval that does nothing, or none
while the ritual sits still.

Schema v2 -> v3 adds four nullable columns to DkgSession -- three approval
timestamps and `approvalRequestedThrough` -- so Room generates the migration. A
ritual already in flight comes back with all three null, which reads as "not
approved yet" and simply asks, rather than silently continuing.

## Being asked

Three screens rather than one parameterised by step, because each is making a
different case and the copy is the substance of the screen, not decoration around
it. They share a scaffold for one reason that is not cosmetic: a screen opened for
one step can go stale -- a redelivery carries the ritual forward, or the member
approves on another device -- so it re-checks the pending step before offering a
button, and `approve` checks again in the manager and ignores a mismatch.

"Not now" does not refuse on the member's behalf. There is no "no" in ChillDKG
short of abandoning the ceremony, and quietly leaving is what a member who is not
ready actually wants; abandoning stays on the ritual screen where the consequence
can be spelled out.

A chat line announces each request, written once per step and guarded by
`approvalRequestedThrough` -- `advance` runs on every arriving message and would
otherwise ask again on each one. It is the one ritual line that asks rather than
reports, so it is the one that is not quiet: primary tint, a Review affordance,
and a tap through to the ritual screen, whose bottom bar routes to the step the
ceremony is actually waiting on.

## Telling the steps apart

The request started as a single message type, which meant one icon for all three
and no way to tell "join the ceremony" from "confirm the key". The type is the
only thing a transcript keeps -- a line drawn days later has no session to ask
what was being requested -- so the step moved into it, one type per step, and
every stage now carries its own icon.

MIGRATION_3_4 rewrites the rows already written. They cannot regenerate: a request
is announced once, so a ceremony already in flight would keep its undifferentiated
icons forever. It changes no schema at all -- the version bump exists only to give
a data rewrite somewhere to run, which is why it is a manual migration on the
builder rather than another AutoMigration. Rows it cannot match keep the old type,
which the renderer still recognises.

An answered request shows a checkmark where Review was. Whether it was answered
comes from the transcript rather than the session: approving is the only thing
that causes the step to be published, and publishing writes an authored line, so a
matching line at or after the request means done. That keeps a room that has run
more than one ceremony correct -- ChatMessage has no session id to disambiguate
with -- and needs no DkgRepository in the message list. The comparison is on
createdAt rather than list position, because the list is ORDER BY createdAt DESC
with reverseLayout, where index arithmetic runs backwards.

Compiles and assembles; the ordering test still passes. No ritual has been run on
a device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 13:43:46 +02:00
Kgothatso Ngako
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>
2026-08-29 19:15:17 +02:00
Kgothatso Ngako
3a830d77d6 Add chat room id on mantra tables 2026-07-19 21:08:44 +02:00
Kgothatso Ngako
3640a77413 Add more associations 2026-07-19 20:58:11 +02:00
Kgothatso Ngako
03c3e56b26 Bug fix for translationChapter primary key 2026-07-19 19:49:07 +02:00
Kgothatso Ngako
ab4d24282d Add mantra models 2026-07-15 22:39:01 +02:00
Kgothatso Ngako
6cdddee424 Rename TorchDatabase to MantraDatabase 2026-07-15 01:19:53 +02:00
Kgothatso Ngako
1bf3ec32ac Process group member changes on commit 2026-07-14 00:31:13 +02:00
Kgothatso Ngako
f53b407e58 Add description to chat room 2026-07-12 21:49:59 +02:00
Kgothatso Ngako
c39b26c2e1 Bug fix for resyncing 2026-07-08 21:19:02 +02:00
Kgothatso Ngako
44f2e7c814 Add signature on MarmotGroupEvent 2026-07-08 20:20:04 +02:00
Kgothatso Ngako
06c726c543 Rename to nostrGroupId and add MarmotCommitResult 2026-07-06 17:03:48 +02:00
Kgothatso Ngako
e175c601d7 Bug fixes 2026-07-04 01:33:06 +02:00
Kgothatso Ngako
bbe77936ed Implement marmot ingestion 2026-07-03 15:02:31 +02:00
Kgothatso Ngako
aad97bdd9e Add deterministic id on MLS related models 2026-07-01 20:27:05 +02:00
Kgothatso Ngako
c47751ad55 Fix to get it to build 2026-06-23 13:59:17 +02:00
Kgothatso Ngako
0f8ee65033 Bug fix on marmot keypackage bundle (Buggy nsec) 2026-06-22 22:30:39 +02:00
Kgothatso Ngako
94723eca6a Pass through the marmot repository 2026-06-19 11:03:38 +03:00
Kgothatso Ngako
ef92a33f56 Bug fix on room creation 2026-06-18 17:54:09 +03:00
Kgothatso Ngako
ffd6450c3b Remove aux DB 2026-06-17 19:02:40 +03:00
Kgothatso Ngako
d20b69a749 Clean up the feed 2026-06-17 19:02:24 +03:00
Kgothatso Ngako
b8ca6b3419 Set receipt for synced messages 2026-06-06 04:35:43 +02:00
Kgothatso Ngako
f3c7e21fa3 Also send a giftWrapMessage to self for message history 2026-06-06 04:25:53 +02:00
Kgothatso Ngako
d140d1a917 Relay hints for chat participants 2026-06-04 14:53:58 +02:00
Kgothatso Ngako
ff7fe8bc4b Add chatMessage relations 2026-06-03 02:57:36 +02:00
Kgothatso Ngako
62582e7569 Introduce giftwrap work flow 2026-06-03 01:14:49 +02:00
Kgothatso Ngako
29f4df66f2 Message formatting 2026-06-02 01:59:00 +02:00
Kgothatso Ngako
29ec692fc4 getOrCreateChat using publicKey 2026-06-01 00:42:48 +02:00
Kgothatso Ngako
c8ba6a0bf0 Foundation for getOrCreate chat room 2026-05-31 20:29:09 +02:00
Kgothatso Ngako
7041fefbbd Add chatroom functionality 2026-05-18 22:52:09 +02:00
Kgothatso Ngako
b604751ab4 Add chat database models 2026-05-18 16:19:36 +02:00
Kgothatso Ngako
58a98f68ae Feed syncs events from twelveHours 2026-05-05 20:14:11 +02:00
Kgothatso Ngako
bbb9d18500 Improve sync 2026-05-05 13:37:38 +02:00
Kgothatso Ngako
2bdb5bc3bf Save nostrEventRelay association 2026-05-04 22:29:22 +02:00
Kgothatso Ngako
e8a42b0123 Introduce foundation for negentropy logic. 2026-05-04 18:12:11 +02:00
Kgothatso Ngako
b9cece869e Change how mentions are handled 2026-05-02 01:19:28 +02:00
Kgothatso Ngako
be3f4390ff Setting up relations 2026-04-30 18:43:18 +02:00
Kgothatso Ngako
c99e3a31c2 Add specific associations on quoted/repost/inreply to 2026-04-28 02:22:29 +02:00
Kgothatso Ngako
23aaa13199 Improve sync logic. 2026-04-22 22:31:03 +02:00
Kgothatso Ngako
2b693b832a Add connection/following/followers db model. 2026-04-22 02:55:22 +02:00
Kgothatso Ngako
4a2ddbc4f2 Correct the namespace and introduce an android specific namespace 2026-04-21 23:46:45 +02:00
Kgothatso Ngako
76858f9cf0 Add primal RelaySocketManager 2026-04-18 02:41:31 +02:00
Kgothatso Ngako
36facaf6a7 Show recent searches from local input 2026-04-02 00:18:29 +02:00
Kgothatso Ngako
b3b93008e5 Prep to show loading on pending sync 2026-04-01 01:47:24 +02:00
Kgothatso Ngako
b01088a74f Sync taggedEvent + taggedAuthor... 2026-03-31 21:01:11 +02:00
Kgothatso Ngako
c3c92d96ad Introduce taggedEvent + taggedAuthor... 2026-03-31 19:48:09 +02:00
Kgothatso Ngako
c1d7572d44 Handling reposts 2026-03-31 16:13:44 +02:00
Kgothatso Ngako
3cefeae5c1 Handle notifications and syncing 2026-03-31 14:29:11 +02:00
Kgothatso Ngako
2beed5e18a Have FilterArray.kt in persisted sync event 2026-03-31 13:44:45 +02:00