feat: hand back a room's running ceremony rather than opening a second
proposeRitual minted a fresh session on every call. Nothing called it twice for the same room, so nothing went wrong: the only way in was the shared-key screen, and canStartRitual() returns false while a session exists that has not failed. That is about to stop being true. A robust group opens a ceremony as it is created, and a NIP-17 room id is derived from its member set -- so making the same group again returns the same room and asks it again. The guard also sat in the wrong place regardless: on an observed UI snapshot, a screen away from the write it was protecting. ## What a second proposal costs It is not a duplicate row. ChillDKG hashes the participant set and the threshold into the session identity, so a second ceremony over the same room is a second `n` and `t` for every member to reconcile, and members join whichever proposal reaches them first -- relays hand gift wraps back in no particular order, so which one that is differs per device. The group ends up split across two ceremonies, neither of which can assemble the participant count it needs. Worse if the first one had finished. FrostSigningManager.completedKey falls back to getLatestSessionForChatRoom when the room has no signed key state and its id is not derived from the threshold key; a newer, unfinished session shadows the completed one there, and the group stops being able to reach the key it actually holds. ## The rule, and where it now lives The room's live ritual is returned as-is, so a caller gets a session either way and cannot tell whether it opened one. That is what makes the creation path safe to re-enter. The rule itself is unchanged -- it is the one canStartRitual() has always applied, right down to which stages block. It now also lives next to the write, where a stale snapshot cannot race it. FAILED is excluded deliberately: it is the one stage that does not hold the room's slot. A collapsed ceremony leaves the group with no key and a room they can still talk in, which is exactly the group that should be able to try again. Every other stage, COMPLETE included, is a ceremony the room depends on the outcome of. Checked before the require()s rather than after. A running ceremony settled the threshold question when it opened, so validating the argument would be validating an input with no effect -- and it would turn re-entering with a different quorum into an exception instead of the ceremony that exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,7 +110,8 @@ object ChillDkgRitualManager {
|
||||
memberPublicKeys(localChatRoom).size >= MINIMUM_PARTICIPANTS
|
||||
|
||||
/**
|
||||
* Opens a ritual, making this device the coordinator.
|
||||
* Opens a ritual, making this device the coordinator. A room already running
|
||||
* one gets that one back rather than a second.
|
||||
*
|
||||
* Throws when the group or the threshold cannot support one; the UI checks
|
||||
* both before offering the button, so reaching either is a bug rather than a
|
||||
@@ -123,6 +124,26 @@ object ChillDkgRitualManager {
|
||||
nostrPrivateKey: ByteArray,
|
||||
threshold: Int
|
||||
): DkgSession {
|
||||
// A room runs one ceremony at a time, and this is reachable twice now that
|
||||
// a robust group opens one as it is created: the room id is derived from
|
||||
// its members, so making the same group again lands back in the same room
|
||||
// and asks again. A second proposal is a second participant set for every
|
||||
// member to reconcile, and the key the first one produced would be left
|
||||
// with nothing pointing at it. A failed ritual is not running, and is
|
||||
// there to be replaced.
|
||||
//
|
||||
// Checked before the arguments are, because a running ritual makes the
|
||||
// requested threshold moot -- it settled that question when it opened.
|
||||
database.dkgSessionDao().getLatestSessionForChatRoom(localChatRoom.chatRoom.id)
|
||||
?.takeIf { it.stage != DkgRitualStage.FAILED }
|
||||
?.let { running ->
|
||||
logger.i(
|
||||
"Room ${localChatRoom.chatRoom.id} is already running ritual " +
|
||||
"${running.id}; not opening another"
|
||||
)
|
||||
return running
|
||||
}
|
||||
|
||||
// Built the way a receiver rebuilds it from the proposal — the p-tags
|
||||
// `broadcast` writes, plus this device — so both sides count the same `n`
|
||||
// even if the room's own rows have drifted.
|
||||
|
||||
Reference in New Issue
Block a user