`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>
179 KiB
179 KiB