fix(tss-android): 修复备份恢复后钱包无法签名的问题
## 问题根因 备份恢复后的钱包在签名时失败,根本原因是 gRPC 通信使用了**设备的 partyId**, 而不是 **share 的原始 partyId**(keygen 时生成的 partyId)。 这导致: 1. 消息订阅使用错误的 partyId,无法接收其他参与方发送的消息 2. 消息发送使用错误的 fromParty,其他参与方无法正确路由消息 3. Session 事件订阅使用错误的 partyId,无法接收 session_started 等事件 4. API 调用使用错误的 partyId,服务端无法正确识别参与方 ## 修改内容 ### 1. 添加新的成员变量用于跟踪正确的 partyId - `currentMessageRoutingPartyId`: 消息路由使用的 partyId - `currentSessionEventPartyId`: Session 事件订阅使用的 partyId ### 2. 修改 startMessageRouting 方法 - 添加 `routingPartyId` 可选参数 - 签名流程中使用 signingPartyId(share 原始 partyId) - 消息发送 (routeMessage fromParty) 使用正确的 partyId - 消息订阅 (subscribeMessages) 使用正确的 partyId ### 3. 修改 startSessionEventSubscription 方法 - 添加 `subscriptionPartyId` 可选参数 - 签名流程中使用 signingPartyId ### 4. 修改 ensureSessionEventSubscriptionActive 方法 - 添加 `signingPartyId` 可选参数 - 支持动态切换订阅的 partyId ### 5. 修复所有签名流程中的调用 #### joinSignSessionViaGrpc 流程: - grpcClient.joinSession 使用 signingPartyId - startMessageRouting 使用 signingPartyId - ensureSessionEventSubscriptionActive 使用 signingPartyId #### joinSignSessionViaApiAndExecute 流程: - joinSignSessionViaApi HTTP 请求使用 signingPartyId - grpcClient.joinSession 使用 signingPartyId - startMessageRouting 使用 signingPartyId #### createSignSession 流程: - ensureSessionEventSubscriptionActive 使用 signingPartyId - join_tokens 查找使用 originalPartyId - grpcClient.joinSession 使用 signingPartyId - startMessageRouting 使用 signingPartyId #### startSigning 流程: - startMessageRouting 使用 signingPartyId ### 6. 修复 joinSignSessionViaApi 函数 - 添加 signingPartyId 参数 - HTTP 请求体中的 party_id 和 device_id 使用 signingPartyId ### 7. 修复重连恢复逻辑 (restoreStreamsAfterReconnect) - startMessageRouting 使用保存的 currentMessageRoutingPartyId - startSessionEventSubscription 使用保存的 currentSessionEventPartyId ## 测试场景 修复后应支持以下场景: 1. 原设备 keygen → 原设备签名 ✓ 2. 原设备 keygen → 备份 → 新设备恢复 → 新设备发起签名 ✓ 3. 原设备 keygen → 备份 → 新设备恢复 → 新设备参与签名 ✓ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
37d3300b17
commit
2799eb5a3a
|
|
@ -108,6 +108,11 @@ class TssRepository @Inject constructor(
|
|||
// Track current message routing params for reconnection recovery
|
||||
private var currentMessageRoutingSessionId: String? = null
|
||||
private var currentMessageRoutingPartyIndex: Int? = null
|
||||
private var currentMessageRoutingPartyId: String? = null // The partyId used for message routing (may differ from device partyId for restored wallets)
|
||||
|
||||
// Track current session event subscription partyId for reconnection recovery
|
||||
// This may differ from device partyId when signing with restored wallets
|
||||
private var currentSessionEventPartyId: String? = null
|
||||
|
||||
// Account service URL (configurable via settings)
|
||||
private var accountServiceUrl: String = "https://rwaapi.szaiai.com"
|
||||
|
|
@ -126,17 +131,19 @@ class TssRepository @Inject constructor(
|
|||
private fun restoreStreamsAfterReconnect() {
|
||||
val sessionId = currentMessageRoutingSessionId
|
||||
val partyIndex = currentMessageRoutingPartyIndex
|
||||
val routingPartyId = currentMessageRoutingPartyId
|
||||
|
||||
// Restore message routing if we had an active session
|
||||
if (sessionId != null && partyIndex != null) {
|
||||
android.util.Log.d("TssRepository", "Restoring message routing for session: $sessionId")
|
||||
startMessageRouting(sessionId, partyIndex)
|
||||
android.util.Log.d("TssRepository", "Restoring message routing for session: $sessionId, routingPartyId: $routingPartyId")
|
||||
startMessageRouting(sessionId, partyIndex, routingPartyId)
|
||||
}
|
||||
|
||||
// Restore session event subscription
|
||||
// Restore session event subscription with the correct partyId
|
||||
if (grpcClient.wasEventStreamSubscribed()) {
|
||||
android.util.Log.d("TssRepository", "Restoring session event subscription")
|
||||
startSessionEventSubscription()
|
||||
val eventPartyId = currentSessionEventPartyId
|
||||
android.util.Log.d("TssRepository", "Restoring session event subscription with partyId: $eventPartyId")
|
||||
startSessionEventSubscription(eventPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -232,12 +239,19 @@ class TssRepository @Inject constructor(
|
|||
|
||||
/**
|
||||
* Start session event subscription (called after registration)
|
||||
*
|
||||
* @param subscriptionPartyId Optional partyId for subscription. If null, uses device partyId.
|
||||
* CRITICAL: For signing with restored wallets, this should be the original partyId
|
||||
* from keygen (shareEntity.partyId) so that session events are received correctly.
|
||||
*/
|
||||
private fun startSessionEventSubscription() {
|
||||
private fun startSessionEventSubscription(subscriptionPartyId: String? = null) {
|
||||
sessionEventJob?.cancel()
|
||||
android.util.Log.d("TssRepository", "Starting session event subscription for partyId: $partyId")
|
||||
val effectivePartyId = subscriptionPartyId ?: partyId
|
||||
// Save for reconnection recovery
|
||||
currentSessionEventPartyId = effectivePartyId
|
||||
android.util.Log.d("TssRepository", "Starting session event subscription for partyId: $effectivePartyId (device partyId: $partyId)")
|
||||
sessionEventJob = repositoryScope.launch {
|
||||
grpcClient.subscribeSessionEvents(partyId).collect { event ->
|
||||
grpcClient.subscribeSessionEvents(effectivePartyId).collect { event ->
|
||||
android.util.Log.d("TssRepository", "=== Session event received ===")
|
||||
android.util.Log.d("TssRepository", " eventType: ${event.eventType}")
|
||||
android.util.Log.d("TssRepository", " sessionId: ${event.sessionId}")
|
||||
|
|
@ -289,20 +303,32 @@ class TssRepository @Inject constructor(
|
|||
* Ensure session event subscription is active
|
||||
* Called before critical operations (like joining sign session) to ensure
|
||||
* the event stream hasn't silently disconnected
|
||||
*
|
||||
* @param signingPartyId Optional partyId for signing operations. If provided,
|
||||
* the subscription will use this partyId instead of the device partyId.
|
||||
* CRITICAL: For signing with restored wallets, this should be the original
|
||||
* partyId from keygen (shareEntity.partyId).
|
||||
*/
|
||||
private fun ensureSessionEventSubscriptionActive() {
|
||||
private fun ensureSessionEventSubscriptionActive(signingPartyId: String? = null) {
|
||||
// Check if the session event job is still active
|
||||
val isActive = sessionEventJob?.isActive == true
|
||||
android.util.Log.d("TssRepository", "Checking session event subscription: isActive=$isActive")
|
||||
val effectivePartyId = signingPartyId ?: currentSessionEventPartyId ?: partyId
|
||||
android.util.Log.d("TssRepository", "Checking session event subscription: isActive=$isActive, effectivePartyId=$effectivePartyId")
|
||||
|
||||
if (!isActive) {
|
||||
android.util.Log.w("TssRepository", "Session event subscription is not active, restarting...")
|
||||
startSessionEventSubscription()
|
||||
startSessionEventSubscription(signingPartyId)
|
||||
} else {
|
||||
// Even if the job is "active", the gRPC stream may have silently disconnected
|
||||
// Force a restart to ensure we have a fresh connection
|
||||
android.util.Log.d("TssRepository", "Refreshing session event subscription to ensure fresh connection")
|
||||
startSessionEventSubscription()
|
||||
// Also restart if we need to switch to a different partyId for signing
|
||||
val needsRestart = signingPartyId != null && signingPartyId != currentSessionEventPartyId
|
||||
if (needsRestart) {
|
||||
android.util.Log.d("TssRepository", "Switching session event subscription to signingPartyId: $signingPartyId")
|
||||
} else {
|
||||
android.util.Log.d("TssRepository", "Refreshing session event subscription to ensure fresh connection")
|
||||
}
|
||||
startSessionEventSubscription(signingPartyId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1186,11 +1212,13 @@ class TssRepository @Inject constructor(
|
|||
_sessionStatus.value = SessionStatus.WAITING
|
||||
|
||||
// Start message subscription (matching Electron's prepareForSign)
|
||||
startMessageRouting(sessionId, myPartyIndex)
|
||||
// CRITICAL: Use signingPartyId (original partyId from keygen) for message routing
|
||||
startMessageRouting(sessionId, myPartyIndex, signingPartyId)
|
||||
|
||||
// CRITICAL: Ensure session event subscription is active
|
||||
// CRITICAL: Ensure session event subscription is active with signingPartyId
|
||||
// The event stream may have silently disconnected, so refresh it
|
||||
ensureSessionEventSubscriptionActive()
|
||||
// Use signingPartyId so that session events for restored wallets are received
|
||||
ensureSessionEventSubscriptionActive(signingPartyId)
|
||||
|
||||
android.util.Log.d("TssRepository", "Sign session state set, waiting for session_started event or in_progress status")
|
||||
|
||||
|
|
@ -1512,8 +1540,15 @@ class TssRepository @Inject constructor(
|
|||
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||
?: return@coroutineScope Result.failure(Exception("Share not found"))
|
||||
|
||||
// CRITICAL: Define signingPartyId BEFORE calling API
|
||||
// Use shareEntity.partyId (original partyId from keygen) for signing
|
||||
val signingPartyId = shareEntity.partyId
|
||||
currentSigningPartyId = signingPartyId // Save for later use in this flow
|
||||
android.util.Log.d("TssRepository", "Using signingPartyId=$signingPartyId for API sign join (device partyId=$partyId)")
|
||||
|
||||
// Step 1: Call account-service API to join sign session and get party info
|
||||
val joinApiResult = joinSignSessionViaApi(inviteCode, shareEntity.partyIndex)
|
||||
// CRITICAL: Pass signingPartyId (original partyId from keygen) to the API
|
||||
val joinApiResult = joinSignSessionViaApi(inviteCode, shareEntity.partyIndex, signingPartyId)
|
||||
if (joinApiResult.isFailure) {
|
||||
return@coroutineScope Result.failure(joinApiResult.exceptionOrNull()!!)
|
||||
}
|
||||
|
|
@ -1522,7 +1557,8 @@ class TssRepository @Inject constructor(
|
|||
android.util.Log.d("TssRepository", "API sign join successful: sessionId=${apiJoinData.sessionId}, partyIndex=${apiJoinData.partyIndex}, messageHash=${apiJoinData.messageHash}")
|
||||
|
||||
// Step 2: Join session via gRPC for message routing
|
||||
val joinResult = grpcClient.joinSession(apiJoinData.sessionId, partyId, apiJoinData.joinToken)
|
||||
// CRITICAL: Use signingPartyId (original partyId from keygen) for backup/restore support
|
||||
val joinResult = grpcClient.joinSession(apiJoinData.sessionId, signingPartyId, apiJoinData.joinToken)
|
||||
if (joinResult.isFailure) {
|
||||
android.util.Log.e("TssRepository", "gRPC join failed", joinResult.exceptionOrNull())
|
||||
return@coroutineScope Result.failure(joinResult.exceptionOrNull()!!)
|
||||
|
|
@ -1547,10 +1583,7 @@ class TssRepository @Inject constructor(
|
|||
_currentSession.value = session
|
||||
_sessionStatus.value = SessionStatus.WAITING
|
||||
|
||||
// Add self to participants
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
|
||||
val signingPartyId = shareEntity.partyId
|
||||
currentSigningPartyId = signingPartyId // Save for later use in this flow
|
||||
// Add self to participants using signingPartyId
|
||||
val allParticipants = sessionData.participants + Participant(signingPartyId, myPartyIndex)
|
||||
|
||||
// Start TSS sign
|
||||
|
|
@ -1573,7 +1606,8 @@ class TssRepository @Inject constructor(
|
|||
_sessionStatus.value = SessionStatus.IN_PROGRESS
|
||||
|
||||
// Start message routing
|
||||
startMessageRouting(apiJoinData.sessionId, myPartyIndex)
|
||||
// CRITICAL: Use signingPartyId (original partyId from keygen) for message routing
|
||||
startMessageRouting(apiJoinData.sessionId, myPartyIndex, signingPartyId)
|
||||
|
||||
// Mark ready - use signingPartyId (original partyId from keygen)
|
||||
grpcClient.markPartyReady(apiJoinData.sessionId, signingPartyId)
|
||||
|
|
@ -1608,8 +1642,13 @@ class TssRepository @Inject constructor(
|
|||
/**
|
||||
* Join sign session via account-service HTTP API
|
||||
* Uses validateSignInviteCode to get session info, then joins
|
||||
*
|
||||
* @param inviteCode The invite code for the sign session
|
||||
* @param partyIndex The party index for this participant
|
||||
* @param signingPartyId The original partyId from keygen (shareEntity.partyId)
|
||||
* CRITICAL: For backup/restore support, this must be the original partyId
|
||||
*/
|
||||
private suspend fun joinSignSessionViaApi(inviteCode: String, partyIndex: Int): Result<ApiJoinSignSessionData> {
|
||||
private suspend fun joinSignSessionViaApi(inviteCode: String, partyIndex: Int, signingPartyId: String): Result<ApiJoinSignSessionData> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// First, get sign session info by invite code
|
||||
|
|
@ -1625,13 +1664,14 @@ class TssRepository @Inject constructor(
|
|||
android.util.Log.d("TssRepository", "Got sign session info: sessionId=$sessionId, messageHash=${signSessionInfo.messageHash}, joinToken=$joinToken")
|
||||
|
||||
// Now call join API (same endpoint as keygen join, but for sign sessions)
|
||||
// CRITICAL: Use signingPartyId (original partyId from keygen) for backup/restore support
|
||||
val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = com.google.gson.JsonObject().apply {
|
||||
addProperty("party_id", partyId)
|
||||
addProperty("party_id", signingPartyId) // Use original partyId from keygen
|
||||
addProperty("join_token", joinToken)
|
||||
addProperty("party_index", partyIndex)
|
||||
addProperty("device_type", "mobile")
|
||||
addProperty("device_id", partyId)
|
||||
addProperty("device_id", signingPartyId) // Use original partyId from keygen
|
||||
}.toString()
|
||||
|
||||
val request = okhttp3.Request.Builder()
|
||||
|
|
@ -1679,21 +1719,35 @@ class TssRepository @Inject constructor(
|
|||
|
||||
/**
|
||||
* Start message routing between TSS and gRPC
|
||||
*
|
||||
* @param sessionId The session ID
|
||||
* @param partyIndex The party index for this participant
|
||||
* @param routingPartyId The party ID to use for message routing.
|
||||
* CRITICAL: For signing with restored wallets, this MUST be the original partyId
|
||||
* from keygen (shareEntity.partyId), not the current device's partyId.
|
||||
* For keygen, this should be the device's partyId.
|
||||
*/
|
||||
private fun startMessageRouting(sessionId: String, partyIndex: Int) {
|
||||
private fun startMessageRouting(sessionId: String, partyIndex: Int, routingPartyId: String? = null) {
|
||||
// Save params for reconnection recovery
|
||||
currentMessageRoutingSessionId = sessionId
|
||||
currentMessageRoutingPartyIndex = partyIndex
|
||||
|
||||
// Use provided routingPartyId, or fall back to device partyId for keygen
|
||||
val effectivePartyId = routingPartyId ?: partyId
|
||||
// Save for reconnection recovery
|
||||
currentMessageRoutingPartyId = effectivePartyId
|
||||
|
||||
messageCollectionJob?.cancel()
|
||||
messageCollectionJob = repositoryScope.launch {
|
||||
android.util.Log.d("TssRepository", "Starting message routing: sessionId=$sessionId, routingPartyId=$effectivePartyId")
|
||||
|
||||
// Collect outgoing messages from TSS and route via gRPC
|
||||
launch {
|
||||
tssNativeBridge.outgoingMessages.collect { message ->
|
||||
val payload = Base64.decode(message.payload, Base64.NO_WRAP)
|
||||
grpcClient.routeMessage(
|
||||
sessionId = sessionId,
|
||||
fromParty = partyId,
|
||||
fromParty = effectivePartyId, // Use the correct partyId for routing
|
||||
toParties = message.toParties ?: emptyList(),
|
||||
roundNumber = 0,
|
||||
messageType = if (message.isBroadcast) "broadcast" else "p2p",
|
||||
|
|
@ -1704,7 +1758,7 @@ class TssRepository @Inject constructor(
|
|||
|
||||
// Collect incoming messages from gRPC and send to TSS
|
||||
launch {
|
||||
grpcClient.subscribeMessages(sessionId, partyId).collect { message ->
|
||||
grpcClient.subscribeMessages(sessionId, effectivePartyId).collect { message ->
|
||||
// Find party index from party ID
|
||||
val session = _currentSession.value
|
||||
val fromPartyIndex = session?.participants?.find { it.partyId == message.fromParty }?.partyIndex
|
||||
|
|
@ -2195,15 +2249,19 @@ class TssRepository @Inject constructor(
|
|||
): Result<SignSessionResult> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// CRITICAL: Ensure session event subscription is active BEFORE creating sign session
|
||||
// This prevents race condition where session_started event arrives before subscription is ready
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Ensuring session event subscription is active before creating sign session")
|
||||
ensureSessionEventSubscriptionActive()
|
||||
|
||||
// Get share record
|
||||
// Get share record first to determine the signingPartyId
|
||||
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||
?: return@withContext Result.failure(Exception("Share not found"))
|
||||
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for session events
|
||||
val signingPartyIdForEvents = shareEntity.partyId
|
||||
|
||||
// CRITICAL: Ensure session event subscription is active BEFORE creating sign session
|
||||
// This prevents race condition where session_started event arrives before subscription is ready
|
||||
// Use signingPartyId so that session events for restored wallets are received
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Ensuring session event subscription is active before creating sign session (signingPartyId=$signingPartyIdForEvents)")
|
||||
ensureSessionEventSubscriptionActive(signingPartyIdForEvents)
|
||||
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Creating sign session for share: ${shareEntity.sessionId}")
|
||||
|
||||
// Step 1: Get keygen session status to get all participants with party_index
|
||||
|
|
@ -2296,20 +2354,26 @@ class TssRepository @Inject constructor(
|
|||
val inviteCode = json.get("invite_code").asString
|
||||
val thresholdT = json.get("threshold_t")?.asInt ?: shareEntity.thresholdT
|
||||
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for join token lookup
|
||||
// The server generates join_tokens keyed by original partyId, not device partyId
|
||||
val originalPartyId = shareEntity.partyId
|
||||
|
||||
// Get join token - support both new format (join_tokens map) and old format (join_token string)
|
||||
val joinToken = if (json.has("join_tokens") && json.get("join_tokens").isJsonObject) {
|
||||
val joinTokens = json.getAsJsonObject("join_tokens")
|
||||
joinTokens.get(partyId)?.asString ?: joinTokens.get("*")?.asString
|
||||
// CRITICAL: Use originalPartyId (from keygen) to look up join token
|
||||
joinTokens.get(originalPartyId)?.asString ?: joinTokens.get("*")?.asString
|
||||
} else {
|
||||
json.get("join_token")?.asString
|
||||
}
|
||||
|
||||
// Build participants list from signingParties
|
||||
// CRITICAL: Use originalPartyId for name comparison (not device partyId)
|
||||
val participants = signingParties.map { (pId, pIndex) ->
|
||||
Participant(
|
||||
partyId = pId,
|
||||
partyIndex = pIndex,
|
||||
name = if (pId == partyId) initiatorName else "参与方 ${pIndex + 1}"
|
||||
name = if (pId == originalPartyId) initiatorName else "参与方 ${pIndex + 1}"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2334,22 +2398,31 @@ class TssRepository @Inject constructor(
|
|||
_sessionStatus.value = SessionStatus.WAITING
|
||||
|
||||
// Step 4: Initiator auto-join via gRPC (matching Electron behavior)
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
|
||||
// This is essential for backup/restore to work correctly
|
||||
val signingPartyId = shareEntity.partyId
|
||||
currentSigningPartyId = signingPartyId // Save for later use in this flow
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Using signingPartyId=$signingPartyId (device partyId=$partyId)")
|
||||
|
||||
var sessionAlreadyInProgress = false
|
||||
if (joinToken != null) {
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Initiator auto-joining session...")
|
||||
val myPartyIndex = signingParties.find { it.first == partyId }?.second ?: shareEntity.partyIndex
|
||||
val myPartyIndex = signingParties.find { it.first == signingPartyId }?.second ?: shareEntity.partyIndex
|
||||
|
||||
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
|
||||
// CRITICAL: Use signingPartyId (original partyId from keygen) for joining
|
||||
val joinResult = grpcClient.joinSession(sessionId, signingPartyId, joinToken)
|
||||
if (joinResult.isSuccess) {
|
||||
val joinData = joinResult.getOrThrow()
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Initiator joined: partyIndex=${joinData.partyIndex}, status=${joinData.sessionStatus}")
|
||||
|
||||
// Step 5: Start message routing (prepareForSign) BEFORE sign starts
|
||||
startMessageRouting(sessionId, myPartyIndex)
|
||||
// CRITICAL: Use signingPartyId for message routing
|
||||
startMessageRouting(sessionId, myPartyIndex, signingPartyId)
|
||||
|
||||
// CRITICAL: Ensure session event subscription is active for sign initiator
|
||||
// The event stream may have silently disconnected
|
||||
ensureSessionEventSubscriptionActive()
|
||||
// Use signingPartyId so that session events for restored wallets are received
|
||||
ensureSessionEventSubscriptionActive(signingPartyId)
|
||||
|
||||
// Step 6: Check if session already in_progress
|
||||
if (joinData.sessionStatus == "in_progress") {
|
||||
|
|
@ -2456,7 +2529,8 @@ class TssRepository @Inject constructor(
|
|||
// Note: Message routing is already started in createSignSession after auto-join
|
||||
// Only start if not already running (for backward compatibility with old flow)
|
||||
if (messageCollectionJob == null || messageCollectionJob?.isActive != true) {
|
||||
startMessageRouting(sessionId, shareEntity.partyIndex)
|
||||
// CRITICAL: Use signingPartyId for message routing
|
||||
startMessageRouting(sessionId, shareEntity.partyIndex, signingPartyId)
|
||||
}
|
||||
|
||||
// Mark ready - use signingPartyId (original partyId from keygen)
|
||||
|
|
|
|||
Loading…
Reference in New Issue