fix(android): fix session_started event race condition with pendingSessionId
Problem: - Android initiator/joiner could miss session_started events due to race condition - Events arriving between joinSession() and _currentSession.value assignment were ignored - This caused keygen timeout because parties never started the TSS protocol Solution: - Add pendingSessionId field set BEFORE joinSession() call - Modify startSessionEventSubscription() to match events against both activeSession and pendingSessionId - Clear pendingSessionId on session completion, failure, or cancellation This ensures session_started events are correctly processed even if they arrive before _currentSession is fully initialized. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cc56b8fadf
commit
eb3f71fa2e
|
|
@ -46,6 +46,10 @@ class TssRepository @Inject constructor(
|
||||||
private var messageCollectionJob: Job? = null
|
private var messageCollectionJob: Job? = null
|
||||||
private var sessionEventJob: Job? = null
|
private var sessionEventJob: Job? = null
|
||||||
|
|
||||||
|
// Pre-registered session ID for event matching (set before joinSession to avoid race condition)
|
||||||
|
// This allows session_started events to be matched even if _currentSession is not yet set
|
||||||
|
private var pendingSessionId: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current party ID
|
* Get the current party ID
|
||||||
*/
|
*/
|
||||||
|
|
@ -161,12 +165,18 @@ class TssRepository @Inject constructor(
|
||||||
android.util.Log.d("TssRepository", " sessionId: ${event.sessionId}")
|
android.util.Log.d("TssRepository", " sessionId: ${event.sessionId}")
|
||||||
android.util.Log.d("TssRepository", " selectedParties: ${event.selectedParties}")
|
android.util.Log.d("TssRepository", " selectedParties: ${event.selectedParties}")
|
||||||
|
|
||||||
// Check if this event is for our active session
|
// Check if this event is for our active session OR pending session
|
||||||
|
// (pendingSessionId handles race condition where session_started arrives before _currentSession is set)
|
||||||
val activeSession = _currentSession.value
|
val activeSession = _currentSession.value
|
||||||
|
val pendingId = pendingSessionId
|
||||||
android.util.Log.d("TssRepository", " activeSession: ${activeSession?.sessionId ?: "null"}")
|
android.util.Log.d("TssRepository", " activeSession: ${activeSession?.sessionId ?: "null"}")
|
||||||
|
android.util.Log.d("TssRepository", " pendingSessionId: ${pendingId ?: "null"}")
|
||||||
|
|
||||||
if (activeSession != null && event.sessionId == activeSession.sessionId) {
|
val matchesActiveSession = activeSession != null && event.sessionId == activeSession.sessionId
|
||||||
android.util.Log.d("TssRepository", " → Event matches active session!")
|
val matchesPendingSession = pendingId != null && event.sessionId == pendingId
|
||||||
|
|
||||||
|
if (matchesActiveSession || matchesPendingSession) {
|
||||||
|
android.util.Log.d("TssRepository", " → Event matches session! (active=$matchesActiveSession, pending=$matchesPendingSession)")
|
||||||
when (event.eventType) {
|
when (event.eventType) {
|
||||||
"session_started" -> {
|
"session_started" -> {
|
||||||
android.util.Log.d("TssRepository", " → Processing session_started event")
|
android.util.Log.d("TssRepository", " → Processing session_started event")
|
||||||
|
|
@ -186,10 +196,10 @@ class TssRepository @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("TssRepository", " → Event does NOT match active session (ignored)")
|
android.util.Log.d("TssRepository", " → Event does NOT match any session (ignored)")
|
||||||
if (activeSession == null) {
|
if (activeSession == null && pendingId == null) {
|
||||||
android.util.Log.d("TssRepository", " Reason: activeSession is null")
|
android.util.Log.d("TssRepository", " Reason: both activeSession and pendingSessionId are null")
|
||||||
} else {
|
} else if (activeSession != null) {
|
||||||
android.util.Log.d("TssRepository", " Reason: sessionId mismatch (event: ${event.sessionId}, active: ${activeSession.sessionId})")
|
android.util.Log.d("TssRepository", " Reason: sessionId mismatch (event: ${event.sessionId}, active: ${activeSession.sessionId})")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,6 +300,12 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "Session created: sessionId=$sessionId, inviteCode=$inviteCode, joinToken length=${joinToken.length}")
|
android.util.Log.d("TssRepository", "Session created: sessionId=$sessionId, inviteCode=$inviteCode, joinToken length=${joinToken.length}")
|
||||||
|
|
||||||
|
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
|
||||||
|
// This ensures session_started events can be matched even if they arrive
|
||||||
|
// before _currentSession is set
|
||||||
|
pendingSessionId = sessionId
|
||||||
|
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching")
|
||||||
|
|
||||||
// Auto-join session via gRPC (matching Electron behavior)
|
// Auto-join session via gRPC (matching Electron behavior)
|
||||||
var partyIndex = 0
|
var partyIndex = 0
|
||||||
var sessionAlreadyInProgress = false
|
var sessionAlreadyInProgress = false
|
||||||
|
|
@ -585,6 +601,12 @@ class TssRepository @Inject constructor(
|
||||||
try {
|
try {
|
||||||
android.util.Log.d("TssRepository", "Joining keygen session via gRPC: sessionId=$sessionId, joinToken length=${joinToken.length}")
|
android.util.Log.d("TssRepository", "Joining keygen session via gRPC: sessionId=$sessionId, joinToken length=${joinToken.length}")
|
||||||
|
|
||||||
|
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
|
||||||
|
// This ensures session_started events can be matched even if they arrive
|
||||||
|
// before _currentSession is set
|
||||||
|
pendingSessionId = sessionId
|
||||||
|
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (joiner)")
|
||||||
|
|
||||||
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
||||||
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
if (joinResult.isFailure) {
|
if (joinResult.isFailure) {
|
||||||
|
|
@ -738,6 +760,7 @@ class TssRepository @Inject constructor(
|
||||||
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
||||||
|
|
||||||
_sessionStatus.value = SessionStatus.COMPLETED
|
_sessionStatus.value = SessionStatus.COMPLETED
|
||||||
|
pendingSessionId = null // Clear pending session ID on completion
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "Keygen as joiner completed: address=$address, partyIndex=$actualPartyIndex")
|
android.util.Log.d("TssRepository", "Keygen as joiner completed: address=$address, partyIndex=$actualPartyIndex")
|
||||||
|
|
||||||
|
|
@ -746,6 +769,7 @@ class TssRepository @Inject constructor(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e)
|
android.util.Log.e("TssRepository", "Execute keygen as joiner failed", e)
|
||||||
_sessionStatus.value = SessionStatus.FAILED
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
pendingSessionId = null // Clear pending session ID on failure
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -783,6 +807,12 @@ class TssRepository @Inject constructor(
|
||||||
|
|
||||||
// Note: Password is verified during actual sign execution, same as Electron
|
// Note: Password is verified during actual sign execution, same as Electron
|
||||||
|
|
||||||
|
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
|
||||||
|
// This ensures session_started events can be matched even if they arrive
|
||||||
|
// before _currentSession is set
|
||||||
|
pendingSessionId = sessionId
|
||||||
|
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (sign joiner)")
|
||||||
|
|
||||||
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
// Join session via gRPC (matching Electron's grpcClient.joinSession)
|
||||||
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||||
if (joinResult.isFailure) {
|
if (joinResult.isFailure) {
|
||||||
|
|
@ -906,6 +936,7 @@ class TssRepository @Inject constructor(
|
||||||
grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes)
|
grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes)
|
||||||
|
|
||||||
_sessionStatus.value = SessionStatus.COMPLETED
|
_sessionStatus.value = SessionStatus.COMPLETED
|
||||||
|
pendingSessionId = null // Clear pending session ID on completion
|
||||||
messageCollectionJob?.cancel()
|
messageCollectionJob?.cancel()
|
||||||
|
|
||||||
android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...")
|
android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...")
|
||||||
|
|
@ -915,6 +946,7 @@ class TssRepository @Inject constructor(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("TssRepository", "Execute sign as joiner failed", e)
|
android.util.Log.e("TssRepository", "Execute sign as joiner failed", e)
|
||||||
_sessionStatus.value = SessionStatus.FAILED
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
pendingSessionId = null // Clear pending session ID on failure
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1437,6 +1469,7 @@ class TssRepository @Inject constructor(
|
||||||
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
grpcClient.reportCompletion(sessionId, partyId, publicKeyBytes)
|
||||||
|
|
||||||
_sessionStatus.value = SessionStatus.COMPLETED
|
_sessionStatus.value = SessionStatus.COMPLETED
|
||||||
|
pendingSessionId = null // Clear pending session ID on completion
|
||||||
sessionEventJob?.cancel()
|
sessionEventJob?.cancel()
|
||||||
|
|
||||||
Result.success(shareEntity.copy(id = id).toShareRecord())
|
Result.success(shareEntity.copy(id = id).toShareRecord())
|
||||||
|
|
@ -1444,6 +1477,7 @@ class TssRepository @Inject constructor(
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("TssRepository", "Start keygen as initiator failed", e)
|
android.util.Log.e("TssRepository", "Start keygen as initiator failed", e)
|
||||||
_sessionStatus.value = SessionStatus.FAILED
|
_sessionStatus.value = SessionStatus.FAILED
|
||||||
|
pendingSessionId = null // Clear pending session ID on failure
|
||||||
Result.failure(e)
|
Result.failure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1458,6 +1492,9 @@ class TssRepository @Inject constructor(
|
||||||
_currentSession.value = null
|
_currentSession.value = null
|
||||||
_sessionStatus.value = SessionStatus.WAITING
|
_sessionStatus.value = SessionStatus.WAITING
|
||||||
|
|
||||||
|
// Clear pending session ID (race condition prevention)
|
||||||
|
pendingSessionId = null
|
||||||
|
|
||||||
// Clear reconnection recovery params
|
// Clear reconnection recovery params
|
||||||
currentMessageRoutingSessionId = null
|
currentMessageRoutingSessionId = null
|
||||||
currentMessageRoutingPartyIndex = null
|
currentMessageRoutingPartyIndex = null
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue