diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt index 670e92f0..35c26a47 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/data/repository/TssRepository.kt @@ -209,6 +209,20 @@ class TssRepository @Inject constructor( // Session event callback (set by ViewModel) private var sessionEventCallback: ((SessionEventData) -> Unit)? = null + // Pending sign info cache (to handle session_started race condition) + // Stores sign session info before session_started event arrives + // Pattern matches server-party-co-managed's PendingSessionCache + private data class PendingSignInfo( + val sessionId: String, + val shareId: Long, + val password: String, + val messageHash: String + ) + private var pendingSignInfo: PendingSignInfo? = null + + // Track if signing has already been triggered (prevent double execution) + private var signingTriggered: Boolean = false + /** * Register this party with the router * Also subscribes to session events (matching Electron behavior) @@ -272,7 +286,48 @@ class TssRepository @Inject constructor( when (event.eventType) { "session_started" -> { android.util.Log.d("TssRepository", " → Processing session_started event") - // Notify callback + + // CRITICAL: Auto-trigger signing if we have cached pendingSignInfo + // This handles race condition where session_started arrives before + // MainViewModel's result.fold completes (typically 4ms race window) + // Pattern matches server-party-co-managed's auto-participation + val signInfo = pendingSignInfo + if (signInfo != null && signInfo.sessionId == event.sessionId && !signingTriggered) { + android.util.Log.d("TssRepository", "[RACE-FIX] session_started arrived! Auto-triggering signing from TssRepository") + android.util.Log.d("TssRepository", "[RACE-FIX] sessionId=${signInfo.sessionId}, shareId=${signInfo.shareId}") + + signingTriggered = true // Mark as triggered BEFORE launching coroutine + + // Launch signing in background (don't block event handler) + repositoryScope.launch { + android.util.Log.d("TssRepository", "[RACE-FIX] Calling startSigning from TssRepository...") + val startResult = startSigning(signInfo.sessionId, signInfo.shareId, signInfo.password) + + if (startResult.isSuccess) { + android.util.Log.d("TssRepository", "[RACE-FIX] startSigning succeeded, calling waitForSignature...") + val signResult = waitForSignature() + android.util.Log.d("TssRepository", "[RACE-FIX] waitForSignature completed: isSuccess=${signResult.isSuccess}") + + // Note: Signature will be available via repository._signature flow + // MainViewModel will pick it up automatically + } else { + android.util.Log.e("TssRepository", "[RACE-FIX] startSigning FAILED: ${startResult.exceptionOrNull()?.message}") + } + + // Clean up after signing (whether success or failure) + pendingSignInfo = null + } + } else { + if (signInfo == null) { + android.util.Log.d("TssRepository", "[RACE-FIX] No pendingSignInfo cached, skipping auto-trigger") + } else if (signInfo.sessionId != event.sessionId) { + android.util.Log.d("TssRepository", "[RACE-FIX] sessionId mismatch (cached=${signInfo.sessionId}, event=${event.sessionId})") + } else if (signingTriggered) { + android.util.Log.d("TssRepository", "[RACE-FIX] Signing already triggered, skipping duplicate") + } + } + + // Notify callback (MainViewModel may also try to trigger, but防重入检查 will prevent duplicate) sessionEventCallback?.invoke(event) } "party_joined", "participant_joined" -> { @@ -339,6 +394,12 @@ class TssRepository @Inject constructor( sessionEventCallback = callback } + /** + * Check if signing has already been triggered (防重入检查) + * Used by MainViewModel to prevent double execution + */ + fun isSigningTriggered(): Boolean = signingTriggered + /** * Set keygen timeout callback (called by ViewModel) * This callback is invoked when the 5-minute polling timeout is reached @@ -3933,6 +3994,19 @@ data class ParticipantStatusInfo( startMessageRouting(sessionId, myPartyIndex, signingPartyId) ensureSessionEventSubscriptionActive(signingPartyId) + // CRITICAL: Cache sign info BEFORE session_started arrives + // This prevents race condition where session_started arrives before + // MainViewModel's result.fold callback completes + // Pattern matches server-party-co-managed's PendingSessionCache + pendingSignInfo = PendingSignInfo( + sessionId = sessionId, + shareId = shareId, + password = password, + messageHash = messageHash + ) + signingTriggered = false // Reset flag for new signing session + android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Cached pendingSignInfo for sessionId=$sessionId to handle session_started event") + if (joinData.sessionStatus == "in_progress") { android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Session already in_progress, will trigger sign immediately") sessionAlreadyInProgress = true diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt index 50d43b27..cdd7fbf3 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt @@ -1470,6 +1470,10 @@ class MainViewModel @Inject constructor( /** * Start sign as initiator (called when session_started event is received) * Matches Electron's handleCoSignStart for initiator + * + * CRITICAL: This method includes防重入检查 to prevent double execution + * Race condition fix: TssRepository may have already triggered signing via + * its session_started handler. This callback serves as a fallback. */ private fun startSignAsInitiator(selectedParties: List) { val info = pendingSignInitiatorInfo @@ -1478,6 +1482,13 @@ class MainViewModel @Inject constructor( return } + // CRITICAL: Prevent double execution if TssRepository already started signing + // TssRepository sets signingTriggered=true when it auto-triggers from session_started + if (repository.isSigningTriggered()) { + android.util.Log.d("MainViewModel", "[RACE-FIX] Signing already triggered by TssRepository, skipping duplicate from MainViewModel") + return + } + android.util.Log.d("MainViewModel", "Starting sign as initiator: sessionId=${info.sessionId}, selectedParties=$selectedParties") startSigningProcess(info.sessionId, info.shareId, info.password) }