KeyRefresh with Reconcile Protocol
Overview
refresh rotates the keyshares of an existing key without changing its public key. Like keygen, it does not overwrite your active share directly: the newly rotated share is written to the staged slot, and a reconcile exchange with the cloud settles it before it replaces the current share.
The difference from keygen is that a refresh has a share to fall back to. The ReconcileStoreDao keeps the previous share in current while the new one sits in staged, so if the reconcile is interrupted or rejected, the key remains fully usable on the old share — nothing is lost, and a share captured before the refresh is useless afterward. This two-slot design is what makes the rotation crash-safe, and your StorageClient is the source of truth the SDK reads back to decide how to finish.
Reconcile runs automatically as the final step of refresh, keygen, and import — in the normal flow you never call it yourself. reconcile(keyId) (returning Result<ByteArray>) is exposed only as a manual retry: call it if a previous operation crashed mid-reconcile and left a staged keyshare that was never promoted to current. The sequence below shows what the SDK does internally, including the rollback path when reconcile does not complete.
Outcomes
A refresh reconcile settles into one of three results:
- Complete — the staged share landed; the SDK promotes it to
currentand clearsstaged. - Already applied — a prior interrupted attempt had already committed; the SDK confirms storage matches.
- Rollback — the staged share is discarded and the previous
currentshare is kept, so the key stays usable exactly as it was before the refresh.
Key Components
Retrying after a crash
refresh, keygen, and import reconcile internally, so you normally never call reconcile yourself. The exception is recovery after a crash: if the app is killed mid-reconcile, the rotated share stays in the staged slot and was never promoted (the key is still fully usable on the current share). Calling reconcile(keyId) on next launch settles it — promoting or rolling back — without re-running the refresh.
Reconciling every stored key on startup closes this gap:
// Run once on app launch, off the main thread.
suspend fun reconcileOnStartup(duoSession: DuoSession, storedKeyIds: List<String>) {
for (keyId in storedKeyIds) {
duoSession.reconcile(keyId)
.onFailure { Log.e("MPC", "reconcile failed for $keyId: ${it.message}") }
}
}