feat(dkg): schema 22 -- a ritual row belongs to a profile, not to a device

`DkgSession.id` is minted by its coordinator and carried on every message, so it
is one string for every participant -- and two profiles on one device are two
participants. Keyed by that id alone there was one row for both of them:
`acceptProposal` asked "do I know this ceremony?", found the row the first
profile wrote, and returned it. The second then ran the ritual out of the first's
row: the first's host key, the first's randomness, and in the end the first's
share, while the protocol had them as two members with two of each.

`679d5249` refuses the second profile's *Welcome*, so this is unreachable for a
Marmot group. A NIP-17 room has no Welcome and no equivalent check, and a group's
own key ceremony is held in exactly such a room, so it was reachable where it
matters most -- at the founding.

**The key is the rule.** `(id, userPublicKey)` is "one row per participant per
ceremony", and `DkgParticipantMessage` gains the receiving profile for two
reasons: a message is an input to *that* profile's recomputation, alongside its
own randomness, and a foreign key needs a unique parent to point at -- a child
keyed by the ceremony alone could not reference the new parent at all, and
deleting a room would leave its rituals' messages behind.

Reads and key are one change, not two. Every query in `DkgSessionDao` now names a
profile, because with this key a ceremony id or a room identifies no row -- it
would return whichever the database ordered first, and hand back that profile's
host key, randomness and share. `completedKey` and `canSign` take the asking
profile and scope all three of their answers by it; `DatabaseDkgRepository` joins
the chat and signing repositories in taking `activeIdentity` at construction, so
a screen asking about "the ceremony" means its own and no call site can forget.

One read names no profile and says so: `getCeremonyThresholdPublicKey` projects a
single public column, the same value on every participant's row, for the two
flows that want the room a ceremony's key derives. Scoping it would make a screen
resolve nothing while no profile is open, for a fact that does not depend on one.

The migration copies both tables into backups holding no foreign key before
dropping anything, which is what makes it safe rather than the drop order:
`DROP TABLE` fires cascades, and whether Room left them enabled is not a thing to
depend on. `DkgSessionPerProfileMigrationJvmTest` runs with foreign keys **on**
and passes with the two drops in either order, which is the evidence.

Schema 22 rather than 21: the subgroups-multiple-parents line allocated 21 while
this was being written, and the rule in docs/curated-to-mantra-profiles.md is that
a version belongs to whichever tree migrates first. 21 leaves both ritual tables
untouched, so the rebuild's DDL is unchanged apart from the versions it runs
between.

`TwoProfilesInOneCeremonyJvmTest` drives both profiles through one ceremony in one
database; all five of its tests fail against the old lookup.
`ProfileScopedSigningKeyJvmTest` covers `completedKey`'s three routes and the
repository seam the UI reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-10-02 19:52:26 +02:00
parent a14ea3d9d3
commit 3c8489ad66
26 changed files with 7508 additions and 153 deletions

File diff suppressed because it is too large Load Diff