Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions app/src/main/kotlin/io/privkey/keep/AccountActions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.os.Handler
import android.os.Looper
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import android.util.Log
import android.widget.Toast
import io.privkey.keep.storage.AndroidKeystoreStorage
Expand Down Expand Up @@ -68,13 +69,21 @@ internal class AccountActions(

private val accountMutex = Mutex()

// Single-flight guard for the DKG. frostCancelDkg sets one process-wide flag
// with no run identity, so a second ceremony queued behind accountMutex (e.g.
// a double-tap on Start before the button hides) would let a cancel abort the
// run actually on the wire while the queued one proceeds. Reject duplicates
// synchronously so only ever one run is in flight for cancel to target.
// Single-flight guard for the DKG. Reject a second ceremony queued behind
// accountMutex (e.g. a double-tap on Start before the button hides) so only
// ever one run is in flight, and so the run id cancel targets is unambiguous.
private val dkgInProgress = AtomicBoolean(false)

// frostCancelDkg now targets a run by id, delivered via
// DkgProgressUpdate.Started at run start. Hold the live run's id so cancelDkg
// can address it; null before Started arrives or between runs.
private val dkgRunId = AtomicReference<ULong?>(null)

// A cancel pressed during the synthesised Connecting window (before Started
// delivers an id) has nothing to target yet. Remember it and apply it the
// moment the id arrives, so an early cancel is honoured rather than dropped.
private val dkgCancelPending = AtomicBoolean(false)

@Volatile
private var currentRelays: List<String> = emptyList()

Expand Down Expand Up @@ -429,8 +438,22 @@ internal class AccountActions(
*/
fun dkgBegin(groupName: String): String = keepMobile.frostDkgBegin(groupName)

/** Signal a cancel to an in-flight [createGroup] DKG run. */
fun cancelDkg() = keepMobile.frostCancelDkg()
/**
* Signal a cancel to an in-flight [createGroup] DKG run.
*
* Set the pending flag first, then read the id: paired with [onProgress]'s
* Started handling (which sets the id first, then reads the flag) this loses
* no cancel across the window where the two race. If the id is already known
* we clear the flag and cancel now; otherwise the flag stays set and Started
* applies it when the id arrives.
*/
fun cancelDkg() {
dkgCancelPending.set(true)
val runId = dkgRunId.get()
if (runId != null && dkgCancelPending.compareAndSet(true, false)) {
keepMobile.frostCancelDkg(runId)
}
}

fun createGroup(
config: DkgConfig,
Expand All @@ -456,8 +479,20 @@ internal class AccountActions(
postState(CreateGroupState.Error(appContext.getString(R.string.create_group_in_progress)))
return
}
// Fresh run: no id yet, and no cancel carried over from a prior run.
dkgRunId.set(null)
dkgCancelPending.set(false)
val callback = object : DkgProgressCallback {
override fun onProgress(update: DkgProgressUpdate) {
if (update is DkgProgressUpdate.Started) {
// Record the id first, then honour any cancel queued during the
// Connecting window. Ordered against cancelDkg (flag-then-id) so
// a cancel racing Started is applied by exactly one side.
dkgRunId.set(update.runId)
if (dkgCancelPending.compareAndSet(true, false)) {
keepMobile.frostCancelDkg(update.runId)
}
}
mainHandler.post { if (!finished.get()) onState(CreateGroupState.Running(update)) }
}
}
Expand Down Expand Up @@ -520,7 +555,11 @@ internal class AccountActions(
// the body: a coroutine launched into an already-cancelled scope never runs
// its body, but invokeOnCompletion still fires (synchronously here), so the
// guard cannot be stranded true and permanently block later ceremonies.
job.invokeOnCompletion { dkgInProgress.set(false) }
job.invokeOnCompletion {
dkgRunId.set(null)
dkgCancelPending.set(false)
dkgInProgress.set(false)
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/kotlin/io/privkey/keep/CreateGroupScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,9 @@ private fun DkgRunView(

@Composable
private fun dkgStatusText(update: DkgProgressUpdate): String = when (update) {
// Started only carries the cancel id; to the user it is the same "connecting"
// moment as Connecting, which follows immediately, so render it identically.
is DkgProgressUpdate.Started -> stringResource(R.string.create_group_status_connecting)
is DkgProgressUpdate.Connecting -> stringResource(R.string.create_group_status_connecting)
is DkgProgressUpdate.Round1 ->
stringResource(R.string.create_group_status_round1, update.received.toInt(), update.total.toInt())
Expand Down
2 changes: 1 addition & 1 deletion keep.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
750d8f5f44304b009ef1c45313999447020dbd9a
d9192f5801949700dbefff2c9634b0fe904edf9e