From 94d283696f5bc5aba6c8db10fbc2a39076679494 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 20 Jan 2026 00:39:05 -0800 Subject: [PATCH] =?UTF-8?q?fix(tss):=20=E4=BF=AE=E5=A4=8D=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E6=81=A2=E5=A4=8D=E5=90=8E=E7=AD=BE=E5=90=8D=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题原因: - 备份恢复的钱包在签名时使用了当前设备的 partyId,而不是原始 keygen 时的 partyId - TSS 协议要求签名时使用的 partyId 必须与 keygen 时完全一致 修复内容: - Android: joinSignSessionViaGrpc() 使用 shareEntity.partyId 而非当前设备 partyId - Electron: cosign:joinSession 和 cosign:createSession 使用 share.party_id - Electron: handleCoSignStart() 使用 share.party_id 进行签名 - 所有 gRPC 通信和消息订阅都使用原始 partyId 关键修改点: - TssRepository.kt: joinSignSessionViaGrpc() 第 1136 行使用 signingPartyId - main.ts: cosign:joinSession 第 1826 行使用 signingPartyId - main.ts: cosign:createSession 第 1624-1633 行使用 share.party_id - main.ts: handleCoSignStart() 第 836 行使用 share.party_id 其他: - 移除 Android APK 中的 x86_64 ABI (仅用于模拟器) Co-Authored-By: Claude Opus 4.5 --- .../app/build.gradle.kts | 3 +- .../tssparty/data/repository/TssRepository.kt | 19 ++++- .../service-party-app/electron/main.ts | 70 ++++++++++++------- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/backend/mpc-system/services/service-party-android/app/build.gradle.kts b/backend/mpc-system/services/service-party-android/app/build.gradle.kts index 2d6dceef..937ab756 100644 --- a/backend/mpc-system/services/service-party-android/app/build.gradle.kts +++ b/backend/mpc-system/services/service-party-android/app/build.gradle.kts @@ -39,8 +39,9 @@ android { } // NDK configuration for TSS native library + // Only include ARM ABIs for real devices (x86_64 is for emulators only) ndk { - abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") + abiFilters += listOf("arm64-v8a", "armeabi-v7a") } } 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 120c1b94..4ac5e70b 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 @@ -1126,14 +1126,26 @@ class TssRepository @Inject constructor( // Note: Password is verified during actual sign execution, same as Electron + // CRITICAL: Use the original partyId from the share (keygen time) for signing + // This is essential for backup/restore - the partyId must match what was used during keygen + // If shareEntity.partyId is empty (legacy data), fall back to current device's partyId + val signingPartyId = if (shareEntity.partyId.isNotEmpty()) { + shareEntity.partyId + } else { + android.util.Log.w("TssRepository", "Share has no partyId (legacy data), using current device partyId") + partyId + } + currentSigningPartyId = signingPartyId // Save for later use in this flow + android.util.Log.d("TssRepository", "Using signingPartyId=$signingPartyId (current device partyId=$partyId)") + // 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) - val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken) + // Join session via gRPC using the original partyId from keygen (CRITICAL for backup/restore) + val joinResult = grpcClient.joinSession(sessionId, signingPartyId, joinToken) if (joinResult.isFailure) { android.util.Log.e("TssRepository", "gRPC sign join failed", joinResult.exceptionOrNull()) return@withContext Result.failure(joinResult.exceptionOrNull()!!) @@ -1148,12 +1160,13 @@ class TssRepository @Inject constructor( // Build participants list (matching Electron's logic) // Prefer using parties from validateInviteCode (complete list) + // CRITICAL: Use signingPartyId (original partyId from keygen) for participant identification val participants = if (parties.isNotEmpty()) { parties.toMutableList() } else { // Fallback: use other_parties + self val list = sessionData.participants.toMutableList() - list.add(Participant(partyId, myPartyIndex, "我")) + list.add(Participant(signingPartyId, myPartyIndex, "我")) list.sortBy { it.partyIndex } list } diff --git a/backend/mpc-system/services/service-party-app/electron/main.ts b/backend/mpc-system/services/service-party-app/electron/main.ts index 278a01a2..2f22e630 100644 --- a/backend/mpc-system/services/service-party-app/electron/main.ts +++ b/backend/mpc-system/services/service-party-app/electron/main.ts @@ -821,6 +821,21 @@ async function handleCoSignStart(event: { // 标记签名开始 signInProgressSessionId = event.sessionId; + // CRITICAL: Get the original partyId from keygen (stored in share) for signing + // This is essential for backup/restore - the partyId must match what was used during keygen + const share = database?.getShare(activeCoSignSession.shareId, activeCoSignSession.sharePassword); + if (!share) { + debugLog.error('main', 'Failed to get share data'); + mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, { + type: 'failed', + error: 'Failed to get share data', + }); + signInProgressSessionId = null; + return; + } + const signingPartyId = share.party_id || grpcClient?.getPartyId() || ''; + debugLog.info('main', `Using signingPartyId=${signingPartyId} (currentDevicePartyId=${grpcClient?.getPartyId()})`); + // 打印当前 activeCoSignSession.participants 状态 console.log('[CO-SIGN] Current activeCoSignSession.participants before update:', activeCoSignSession.participants.map(p => ({ @@ -832,8 +847,9 @@ async function handleCoSignStart(event: { // 从 event.selectedParties 更新参与者列表 // 优先使用 activeCoSignSession.participants 中的 partyIndex(来自 signingParties 或 other_parties) + // CRITICAL: Use signingPartyId (original from keygen) for identification if (event.selectedParties && event.selectedParties.length > 0) { - const myPartyId = grpcClient?.getPartyId(); + const myPartyId = signingPartyId; const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; event.selectedParties.forEach((partyId) => { @@ -869,21 +885,11 @@ async function handleCoSignStart(event: { }))); } - // 获取 share 数据 - const share = database?.getShare(activeCoSignSession.shareId, activeCoSignSession.sharePassword); - if (!share) { - debugLog.error('main', 'Failed to get share data'); - mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, { - type: 'failed', - error: 'Failed to get share data', - }); - signInProgressSessionId = null; - return; - } + // Note: share already fetched above for getting signingPartyId console.log('[CO-SIGN] Calling tssHandler.participateSign with:', { sessionId: activeCoSignSession.sessionId, - partyId: grpcClient?.getPartyId(), + partyId: signingPartyId, // CRITICAL: Use signingPartyId (original from keygen) partyIndex: activeCoSignSession.partyIndex, participants: activeCoSignSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })), threshold: activeCoSignSession.threshold, @@ -892,9 +898,10 @@ async function handleCoSignStart(event: { debugLog.info('tss', `Starting sign for session ${event.sessionId}...`); try { + // CRITICAL: Use signingPartyId (original partyId from keygen) for signing const result = await (tssHandler as TSSHandler).participateSign( activeCoSignSession.sessionId, - grpcClient?.getPartyId() || '', + signingPartyId, // CRITICAL: Use original partyId from keygen for backup/restore to work activeCoSignSession.partyIndex, activeCoSignSession.participants, activeCoSignSession.threshold, @@ -1613,9 +1620,9 @@ function setupIpcHandlers() { initiatorName?: string; }) => { try { - // 获取当前 party ID - const partyId = grpcClient?.getPartyId(); - if (!partyId) { + // 获取当前 party ID (用于检查连接状态) + const currentDevicePartyId = grpcClient?.getPartyId(); + if (!currentDevicePartyId) { return { success: false, error: '请先连接到消息路由器' }; } @@ -1625,6 +1632,11 @@ function setupIpcHandlers() { return { success: false, error: 'Share 不存在或密码错误' }; } + // CRITICAL: Use the original partyId from keygen (stored in share) for signing + // This is essential for backup/restore - the partyId must match what was used during keygen + const partyId = share.party_id || currentDevicePartyId; + debugLog.info('main', `Initiator using partyId=${partyId} (currentDevicePartyId=${currentDevicePartyId})`); + // 从后端获取 keygen 会话的参与者信息(包含正确的 party_index) const keygenStatus = await accountClient?.getSessionStatus(share.session_id); if (!keygenStatus?.participants || keygenStatus.participants.length === 0) { @@ -1810,8 +1822,8 @@ function setupIpcHandlers() { parties?: Array<{ party_id: string; party_index: number }>; }) => { try { - const partyId = grpcClient?.getPartyId(); - if (!partyId) { + const currentDevicePartyId = grpcClient?.getPartyId(); + if (!currentDevicePartyId) { return { success: false, error: '请先连接到消息路由器' }; } @@ -1821,9 +1833,12 @@ function setupIpcHandlers() { return { success: false, error: 'Share 不存在或密码错误' }; } - debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, partyId=${partyId}`); + // CRITICAL: Use the original partyId from keygen (stored in share) for signing + // This is essential for backup/restore - the partyId must match what was used during keygen + const signingPartyId = share.party_id || currentDevicePartyId; + debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, signingPartyId=${signingPartyId} (currentDevicePartyId=${currentDevicePartyId})`); - const result = await grpcClient?.joinSession(params.sessionId, partyId, params.joinToken); + const result = await grpcClient?.joinSession(params.sessionId, signingPartyId, params.joinToken); if (result?.success) { // 设置活跃的 Co-Sign 会话 // 优先使用 params.parties(来自 validateInviteCode,包含所有预期参与者) @@ -1832,10 +1847,11 @@ function setupIpcHandlers() { if (params.parties && params.parties.length > 0) { // 使用完整的 parties 列表 + // CRITICAL: Use signingPartyId (original from keygen) for identification participants = params.parties.map(p => ({ partyId: p.party_id, partyIndex: p.party_index, - name: p.party_id === partyId ? '我' : `参与方 ${p.party_index + 1}`, + name: p.party_id === signingPartyId ? '我' : `参与方 ${p.party_index + 1}`, })); console.log('[CO-SIGN] Participant using params.parties (complete list):', participants.map(p => ({ partyId: p.partyId.substring(0, 8), @@ -1850,9 +1866,9 @@ function setupIpcHandlers() { name: `参与方 ${idx + 1}`, })) || []; - // 添加自己 + // 添加自己 - CRITICAL: Use signingPartyId (original from keygen) participants.push({ - partyId: partyId, + partyId: signingPartyId, partyIndex: result.party_index, name: '我', }); @@ -1886,11 +1902,11 @@ function setupIpcHandlers() { messageHash: params.messageHash, }); - // 预订阅消息流 + // 预订阅消息流 - CRITICAL: Use signingPartyId (original from keygen) if (tssHandler && 'prepareForSign' in tssHandler) { try { - debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}`); - (tssHandler as TSSHandler).prepareForSign(params.sessionId, partyId); + debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}, signingPartyId=${signingPartyId}`); + (tssHandler as TSSHandler).prepareForSign(params.sessionId, signingPartyId); } catch (prepareErr) { debugLog.error('tss', `Failed to prepare for sign: ${(prepareErr as Error).message}`); return { success: false, error: `消息订阅失败: ${(prepareErr as Error).message}` };