diff --git a/backend/docker-compose.windows.yml b/backend/docker-compose.windows.yml index 696559b6..7c1f11a3 100644 --- a/backend/docker-compose.windows.yml +++ b/backend/docker-compose.windows.yml @@ -159,6 +159,7 @@ services: dockerfile: services/message-router/Dockerfile container_name: mpc-message-router ports: + - "50051:50051" # gRPC for party connections - "8082:8080" environment: MPC_SERVER_GRPC_PORT: 50051 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 adf70130..12ca0435 100644 --- a/backend/mpc-system/services/service-party-app/electron/main.ts +++ b/backend/mpc-system/services/service-party-app/electron/main.ts @@ -77,6 +77,26 @@ let activeKeygenSession: ActiveKeygenSession | null = null; // Keygen 幂等性保护:追踪正在进行的 keygen 会话 ID let keygenInProgressSessionId: string | null = null; +// =========================================================================== +// Co-Sign 相关状态 +// =========================================================================== + +// 当前正在进行的 Co-Sign 会话信息 +interface ActiveCoSignSession { + sessionId: string; + partyIndex: number; + participants: Array<{ partyId: string; partyIndex: number; name: string }>; + threshold: { t: number; n: number }; + walletName: string; + messageHash: string; + shareId: string; + sharePassword: string; +} +let activeCoSignSession: ActiveCoSignSession | null = null; + +// Co-Sign 幂等性保护:追踪正在进行的签名会话 ID +let signInProgressSessionId: string | null = null; + // 会话事件缓存 - 解决前端订阅时可能错过事件的时序问题 // 当事件到达时,前端可能还在页面导航中,尚未订阅 interface SessionEventData { @@ -619,6 +639,295 @@ async function handleKeygenComplete(result: KeygenResult) { } } +// =========================================================================== +// Co-Sign 相关处理函数 +// =========================================================================== + +// 检查并触发 Co-Sign(在收到 all_joined 事件后调用) +async function checkAndTriggerCoSign(sessionId: string) { + console.log('[CO-SIGN] checkAndTriggerCoSign called with sessionId:', sessionId); + + if (!activeCoSignSession || activeCoSignSession.sessionId !== sessionId) { + console.log('[CO-SIGN] No matching active co-sign session for', sessionId); + return; + } + + console.log('[CO-SIGN] Active session found:', { + sessionId: activeCoSignSession.sessionId, + partyIndex: activeCoSignSession.partyIndex, + threshold: activeCoSignSession.threshold, + participantCount: activeCoSignSession.participants.length, + }); + + // 如果 TSS 已经在运行,不重复触发 + if (tssHandler?.getIsRunning()) { + console.log('[CO-SIGN] TSS already running, skip check'); + return; + } + + const pollIntervalMs = 2000; // 2秒轮询间隔 + const maxWaitMs = 5 * 60 * 1000; // 5分钟超时 + const startTime = Date.now(); + + console.log('[CO-SIGN] Starting to poll session status...'); + debugLog.info('main', `Starting to poll co-sign session status for ${sessionId}`); + + while (Date.now() - startTime < maxWaitMs) { + // 检查会话是否仍然有效 + if (!activeCoSignSession || activeCoSignSession.sessionId !== sessionId) { + debugLog.warn('main', 'Active co-sign session changed, stopping poll'); + return; + } + + // 如果 TSS 已经在运行,停止轮询 + if (tssHandler?.getIsRunning()) { + debugLog.info('main', 'TSS started running, stopping poll'); + return; + } + + try { + // 获取签名会话状态 + const status = await accountClient?.getSignSessionStatus(sessionId); + if (!status) { + debugLog.warn('main', 'Failed to get sign session status, will retry'); + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + continue; + } + + const expectedT = activeCoSignSession.threshold.t; + const currentParticipants = status.joined_count || 0; + + debugLog.debug('main', `Sign session ${sessionId} status: ${status.status}, participants: ${currentParticipants}/${expectedT}`); + + // 检查是否满足启动条件 + const hasAllParticipants = currentParticipants >= expectedT; + const statusReady = status.status === 'in_progress' || + status.status === 'all_joined' || + status.status === 'waiting_for_sign'; + + console.log('[CO-SIGN] Check conditions:', { + hasAllParticipants, + statusReady, + currentParticipants, + expectedT, + status: status.status, + }); + + if (hasAllParticipants && statusReady) { + console.log('[CO-SIGN] Conditions met! Triggering sign...'); + debugLog.info('main', `All ${expectedT} participants joined (status: ${status.status}), triggering sign...`); + + // 更新参与者列表 + if (status.parties && status.parties.length > 0) { + const myPartyId = grpcClient?.getPartyId(); + const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; + + for (const p of status.parties) { + const existing = activeCoSignSession.participants.find(ep => ep.partyId === p.party_id); + updatedParticipants.push({ + partyId: p.party_id, + partyIndex: p.party_index, + name: existing?.name || (p.party_id === myPartyId ? '我' : `参与方 ${p.party_index + 1}`), + }); + } + + activeCoSignSession.participants = updatedParticipants; + + const myInfo = updatedParticipants.find(p => p.partyId === myPartyId); + if (myInfo) { + activeCoSignSession.partyIndex = myInfo.partyIndex; + } + } + + const selectedParties = activeCoSignSession.participants.map(p => p.partyId); + + await handleCoSignStart({ + eventType: 'session_started', + sessionId: sessionId, + thresholdT: activeCoSignSession.threshold.t, + thresholdN: activeCoSignSession.threshold.n, + selectedParties: selectedParties, + }); + + return; + } + + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + + } catch (error) { + debugLog.error('main', `Failed to check sign session status: ${error}, will retry`); + await new Promise(resolve => setTimeout(resolve, pollIntervalMs)); + } + } + + // 超时 + debugLog.error('main', `Timeout: failed to start sign within 5 minutes for session ${sessionId}`); + + if (mainWindow) { + mainWindow.webContents.send(`cosign:events:${sessionId}`, { + type: 'sign_start_timeout', + error: '启动签名超时,请重试', + }); + } +} + +// 处理 Co-Sign 会话开始事件 - 触发签名 +async function handleCoSignStart(event: { + eventType: string; + sessionId: string; + thresholdT: number; + thresholdN: number; + selectedParties: string[]; +}) { + console.log('[CO-SIGN] handleCoSignStart called:', { + eventType: event.eventType, + sessionId: event.sessionId, + thresholdT: event.thresholdT, + thresholdN: event.thresholdN, + selectedParties: event.selectedParties?.length, + }); + + if (!activeCoSignSession) { + console.log('[CO-SIGN] No active co-sign session, ignoring'); + debugLog.debug('main', 'No active co-sign session, ignoring sign start event'); + return; + } + + if (activeCoSignSession.sessionId !== event.sessionId) { + debugLog.debug('main', `Session ID mismatch: expected ${activeCoSignSession.sessionId}, got ${event.sessionId}`); + return; + } + + // 幂等性保护 + if (signInProgressSessionId === event.sessionId) { + debugLog.debug('main', `Sign already in progress for session ${event.sessionId}, skipping duplicate trigger`); + return; + } + + if (tssHandler?.getIsRunning()) { + debugLog.debug('main', 'TSS already running, skipping'); + return; + } + + if (!tssHandler || !('participateSign' in tssHandler)) { + debugLog.error('tss', 'TSS handler not initialized or does not support signing'); + mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, { + type: 'failed', + error: 'TSS handler not initialized', + }); + return; + } + + // 标记签名开始 + signInProgressSessionId = event.sessionId; + + // 获取 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; + } + + console.log('[CO-SIGN] Calling tssHandler.participateSign with:', { + sessionId: activeCoSignSession.sessionId, + partyId: grpcClient?.getPartyId(), + partyIndex: activeCoSignSession.partyIndex, + participants: activeCoSignSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })), + threshold: activeCoSignSession.threshold, + messageHash: activeCoSignSession.messageHash.substring(0, 16) + '...', + }); + debugLog.info('tss', `Starting sign for session ${event.sessionId}...`); + + try { + const result = await (tssHandler as TSSHandler).participateSign( + activeCoSignSession.sessionId, + grpcClient?.getPartyId() || '', + activeCoSignSession.partyIndex, + activeCoSignSession.participants, + activeCoSignSession.threshold, + activeCoSignSession.messageHash, + share.raw_share, + activeCoSignSession.sharePassword + ); + + if (result.success) { + debugLog.info('tss', 'Sign completed successfully'); + await handleCoSignComplete(result); + } else { + debugLog.error('tss', `Sign failed: ${result.error}`); + mainWindow?.webContents.send(`cosign:events:${activeCoSignSession.sessionId}`, { + type: 'failed', + error: result.error || 'Sign failed', + }); + signInProgressSessionId = null; + } + } catch (error) { + debugLog.error('tss', `Sign error: ${(error as Error).message}`); + mainWindow?.webContents.send(`cosign:events:${activeCoSignSession?.sessionId}`, { + type: 'failed', + error: (error as Error).message, + }); + signInProgressSessionId = null; + } +} + +// 处理 Co-Sign 完成 - 保存签名并报告完成 +async function handleCoSignComplete(result: { success: boolean; signature: Buffer; error?: string }) { + if (!activeCoSignSession || !database || !grpcClient) { + debugLog.error('main', 'Missing required components for sign completion'); + return; + } + + const sessionId = activeCoSignSession.sessionId; + const partyId = grpcClient.getPartyId(); + + try { + const signatureHex = result.signature.toString('hex'); + + // 1. 更新签名历史 + database.updateSigningHistory(sessionId, { + status: 'completed', + signature: signatureHex, + }); + + debugLog.info('main', `Signature saved: ${signatureHex.substring(0, 32)}...`); + + // 2. 报告完成给 session-coordinator + const allCompleted = await grpcClient.reportCompletion( + sessionId, + partyId || '', + result.signature + ); + + debugLog.info('grpc', `Reported sign completion to session-coordinator, all_completed: ${allCompleted}`); + + // 3. 通知前端 + mainWindow?.webContents.send(`cosign:events:${sessionId}`, { + type: 'completed', + signature: signatureHex, + allCompleted: allCompleted, + }); + + // 4. 清理活跃会话和幂等性标志 + activeCoSignSession = null; + signInProgressSessionId = null; + debugLog.info('main', 'Co-Sign session completed and cleaned up'); + + } catch (error) { + debugLog.error('main', `Failed to handle sign completion: ${error}`); + mainWindow?.webContents.send(`cosign:events:${sessionId}`, { + type: 'failed', + error: (error as Error).message, + }); + signInProgressSessionId = null; + } +} + // 连接并注册到 Message Router async function connectAndRegisterToMessageRouter() { if (!grpcClient || !database) { @@ -688,26 +997,53 @@ async function connectAndRegisterToMessageRouter() { // 转发事件到前端 mainWindow?.webContents.send(`session:events:${event.session_id}`, eventData); - // 根据事件类型处理 + // 根据事件类型处理 - 区分 Keygen 和 Co-Sign 会话 + const isCoSignSession = activeCoSignSession?.sessionId === event.session_id; + const isKeygenSession = activeKeygenSession?.sessionId === event.session_id; + if (event.event_type === 'all_joined') { // 收到 all_joined 事件表示所有参与方都已加入 - // 此时启动 5 分钟倒计时,在此期间完成 keygen 启动 - debugLog.info('main', `Received all_joined event for session ${event.session_id}, starting 5-minute keygen countdown`); + debugLog.info('main', `Received all_joined event for session ${event.session_id}, isCoSign=${isCoSignSession}, isKeygen=${isKeygenSession}`); - // 使用 setImmediate 确保不阻塞事件处理 - setImmediate(() => { - checkAndTriggerKeygen(event.session_id); - }); + if (isCoSignSession) { + // Co-Sign 会话:转发到 cosign 频道并触发签名 + mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData); + setImmediate(() => { + checkAndTriggerCoSign(event.session_id); + }); + } else if (isKeygenSession) { + // Keygen 会话:启动 5 分钟倒计时 + setImmediate(() => { + checkAndTriggerKeygen(event.session_id); + }); + } } else if (event.event_type === 'session_started') { - // session_started 事件表示 keygen 可以开始了(所有人已准备就绪) - // 直接触发 keygen - await handleSessionStart({ - eventType: event.event_type, - sessionId: event.session_id, - thresholdN: event.threshold_n, - thresholdT: event.threshold_t, - selectedParties: event.selected_parties, - }); + // session_started 事件表示可以开始了 + if (isCoSignSession) { + // Co-Sign 会话 + mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData); + await handleCoSignStart({ + eventType: event.event_type, + sessionId: event.session_id, + thresholdN: event.threshold_n, + thresholdT: event.threshold_t, + selectedParties: event.selected_parties, + }); + } else if (isKeygenSession) { + // Keygen 会话 + await handleSessionStart({ + eventType: event.event_type, + sessionId: event.session_id, + thresholdN: event.threshold_n, + thresholdT: event.threshold_t, + selectedParties: event.selected_parties, + }); + } + } else if (event.event_type === 'participant_joined') { + // 参与者加入事件 - 也需要区分会话类型 + if (isCoSignSession) { + mainWindow?.webContents.send(`cosign:events:${event.session_id}`, eventData); + } } }); } catch (error) { @@ -1206,6 +1542,317 @@ function setupIpcHandlers() { return accountClient?.getBaseUrl() || 'https://rwaapi.szaiai.com'; }); + // =========================================================================== + // Co-Sign 相关 IPC 处理器 + // =========================================================================== + + // 创建 Co-Sign 会话 + ipcMain.handle('cosign:createSession', async (_event, params: { + shareId: string; + sharePassword: string; + messageHash: string; + initiatorName?: string; + }) => { + try { + // 获取当前 party ID + const partyId = grpcClient?.getPartyId(); + if (!partyId) { + return { success: false, error: '请先连接到消息路由器' }; + } + + // 从本地获取 share 信息 + const share = database?.getShare(params.shareId, params.sharePassword); + if (!share) { + return { success: false, error: 'Share 不存在或密码错误' }; + } + + // 解析参与者信息 + const participants = JSON.parse(share.participants_json || '[]'); + const parties = participants.map((p: { partyId: string }, index: number) => ({ + party_id: p.partyId, + party_index: index, + })); + + // 创建签名会话 + const result = await accountClient?.createSignSession({ + keygen_session_id: share.session_id, + wallet_name: share.wallet_name, + message_hash: params.messageHash, + parties: parties, + threshold_t: share.threshold_t, + initiator_name: params.initiatorName || '发起者', + }); + + if (!result?.session_id) { + return { success: false, error: '创建签名会话失败: 未返回会话ID' }; + } + + // 创建签名历史记录 + database?.createSigningHistory({ + shareId: params.shareId, + sessionId: result.session_id, + messageHash: params.messageHash, + }); + + // 发起方自动加入会话 + const joinToken = result.join_token; + if (joinToken) { + console.log('[CO-SIGN] Initiator auto-joining session...'); + const joinResult = await grpcClient?.joinSession(result.session_id, partyId, joinToken); + + if (joinResult?.success) { + // 设置活跃的 Co-Sign 会话信息 + const signParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = []; + + // 添加发起方 + signParticipants.push({ + partyId: partyId, + partyIndex: joinResult.party_index, + name: params.initiatorName || '发起者', + }); + + activeCoSignSession = { + sessionId: result.session_id, + partyIndex: joinResult.party_index, + participants: signParticipants, + threshold: { + t: share.threshold_t, + n: share.threshold_n, + }, + walletName: share.wallet_name, + messageHash: params.messageHash, + shareId: params.shareId, + sharePassword: params.sharePassword, + }; + + console.log('[CO-SIGN] Initiator active session set:', { + sessionId: activeCoSignSession.sessionId, + partyIndex: activeCoSignSession.partyIndex, + threshold: activeCoSignSession.threshold, + }); + + // 预订阅消息流 + if (tssHandler && 'prepareForSign' in tssHandler) { + debugLog.info('tss', `Initiator preparing for sign: subscribing to messages for session ${result.session_id}`); + (tssHandler as TSSHandler).prepareForSign(result.session_id, partyId); + } + + // 检查会话状态 + const sessionStatus = joinResult.session_info?.status; + debugLog.info('main', `Initiator JoinSession response: status=${sessionStatus}`); + + if (sessionStatus === 'in_progress') { + debugLog.info('main', 'Session already in_progress, triggering sign immediately'); + setImmediate(async () => { + const selectedParties = activeCoSignSession?.participants.map(p => p.partyId) || []; + await handleCoSignStart({ + eventType: 'session_started', + sessionId: result.session_id, + thresholdT: share.threshold_t, + thresholdN: share.threshold_n, + selectedParties: selectedParties, + }); + }); + } + } else { + console.warn('[CO-SIGN] Initiator failed to join session'); + } + } + + return { + success: true, + sessionId: result.session_id, + inviteCode: result.invite_code, + walletName: share.wallet_name, + expiresAt: result.expires_at, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 验证 Co-Sign 邀请码 + ipcMain.handle('cosign:validateInviteCode', async (_event, { code }) => { + try { + debugLog.info('account', `Validating co-sign invite code: ${code}`); + const result = await accountClient?.getSignSessionByInviteCode(code); + + if (!result?.session_id) { + return { success: false, error: '无效的邀请码' }; + } + + return { + success: true, + sessionInfo: { + sessionId: result.session_id, + keygenSessionId: result.keygen_session_id, + walletName: result.wallet_name, + messageHash: result.message_hash, + threshold: { + t: result.threshold_t, + n: result.parties?.length || 0, + }, + status: result.status, + currentParticipants: result.joined_count || 0, + parties: result.parties, + }, + joinToken: result.join_token, + }; + } catch (error) { + debugLog.error('account', `Failed to validate co-sign invite code: ${(error as Error).message}`); + return { success: false, error: (error as Error).message }; + } + }); + + // 加入 Co-Sign 会话 + ipcMain.handle('cosign:joinSession', async (_event, params: { + sessionId: string; + shareId: string; + sharePassword: string; + joinToken: string; + walletName?: string; + messageHash: string; + threshold: { t: number; n: number }; + }) => { + try { + const partyId = grpcClient?.getPartyId(); + if (!partyId) { + return { success: false, error: '请先连接到消息路由器' }; + } + + // 验证 share + const share = database?.getShare(params.shareId, params.sharePassword); + if (!share) { + return { success: false, error: 'Share 不存在或密码错误' }; + } + + debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, partyId=${partyId}`); + + const result = await grpcClient?.joinSession(params.sessionId, partyId, params.joinToken); + if (result?.success) { + // 设置活跃的 Co-Sign 会话 + const participants: Array<{ partyId: string; partyIndex: number; name: string }> = result.other_parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({ + partyId: p.party_id, + partyIndex: p.party_index, + name: `参与方 ${idx + 1}`, + })) || []; + + // 添加自己 + participants.push({ + partyId: partyId, + partyIndex: result.party_index, + name: '我', + }); + + // 按 partyIndex 排序 + participants.sort((a, b) => a.partyIndex - b.partyIndex); + + activeCoSignSession = { + sessionId: params.sessionId, + partyIndex: result.party_index, + participants: participants, + threshold: params.threshold, + walletName: params.walletName || share.wallet_name, + messageHash: params.messageHash, + shareId: params.shareId, + sharePassword: params.sharePassword, + }; + + console.log('[CO-SIGN] Active session set:', { + sessionId: activeCoSignSession.sessionId, + partyIndex: activeCoSignSession.partyIndex, + participantCount: activeCoSignSession.participants.length, + threshold: activeCoSignSession.threshold, + }); + + // 创建签名历史记录 + database?.createSigningHistory({ + shareId: params.shareId, + sessionId: params.sessionId, + messageHash: params.messageHash, + }); + + // 预订阅消息流 + if (tssHandler && 'prepareForSign' in tssHandler) { + debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}`); + (tssHandler as TSSHandler).prepareForSign(params.sessionId, partyId); + } + + // 检查会话状态 + const sessionStatus = result.session_info?.status; + debugLog.info('main', `JoinSession response: status=${sessionStatus}`); + + if (sessionStatus === 'in_progress') { + debugLog.info('main', 'Session already in_progress, triggering sign immediately'); + setImmediate(async () => { + const selectedParties = activeCoSignSession?.participants.map(p => p.partyId) || []; + await handleCoSignStart({ + eventType: 'session_started', + sessionId: params.sessionId, + thresholdT: params.threshold.t, + thresholdN: params.threshold.n, + selectedParties: selectedParties, + }); + }); + } + + return { success: true, data: result }; + } else { + return { success: false, error: '加入会话失败' }; + } + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 获取 Co-Sign 会话状态 + ipcMain.handle('cosign:getSessionStatus', async (_event, { sessionId }) => { + try { + const result = await accountClient?.getSignSessionStatus(sessionId); + return { + success: true, + session: { + sessionId: result?.session_id, + status: result?.status, + joinedCount: result?.joined_count, + threshold: { + t: activeCoSignSession?.threshold?.t || 0, + n: activeCoSignSession?.threshold?.n || 0, + }, + participants: result?.parties?.map((p: { party_id: string; party_index: number }, idx: number) => ({ + partyId: p.party_id, + partyIndex: p.party_index, + name: activeCoSignSession?.participants?.find(ap => ap.partyId === p.party_id)?.name || `参与方 ${idx + 1}`, + status: 'ready', + })) || [], + messageHash: activeCoSignSession?.messageHash || '', + walletName: activeCoSignSession?.walletName || '', + }, + }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + + // 订阅 Co-Sign 会话事件 + ipcMain.on('cosign:subscribeSessionEvents', (_event, { sessionId }) => { + debugLog.debug('main', `Frontend subscribing to co-sign session events: ${sessionId}`); + + // 获取并发送缓存的事件 + const cachedEvents = getAndClearCachedEvents(sessionId); + if (cachedEvents.length > 0) { + debugLog.info('main', `Sending ${cachedEvents.length} cached events to frontend for co-sign session ${sessionId}`); + for (const event of cachedEvents) { + mainWindow?.webContents.send(`cosign:events:${sessionId}`, event); + } + } + }); + + // 取消订阅 Co-Sign 会话事件 + ipcMain.on('cosign:unsubscribeSessionEvents', (_event, { sessionId }) => { + debugLog.debug('main', `Frontend unsubscribing from co-sign session events: ${sessionId}`); + }); + // =========================================================================== // Share 存储相关 (SQLite) // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts b/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts index c0e16dcc..d1d6dc9c 100644 --- a/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts +++ b/backend/mpc-system/services/service-party-app/electron/modules/account-client.ts @@ -137,6 +137,17 @@ export interface GetSignSessionByInviteCodeResponse { expires_at: number; parties: SignPartyInfo[]; joined_count: number; + join_token?: string; +} + +export interface GetSignSessionStatusResponse { + session_id: string; + status: string; + threshold_t: number; + joined_count: number; + parties: SignPartyInfo[]; + message_hash?: string; + signature?: string; } // 错误响应 @@ -313,6 +324,16 @@ export class AccountClient { ); } + /** + * 获取 Sign 会话状态 + */ + async getSignSessionStatus(sessionId: string): Promise { + return this.request( + 'GET', + `/api/v1/co-managed/sign/${sessionId}` + ); + } + // =========================================================================== // 健康检查 // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/electron/modules/tss-handler.ts b/backend/mpc-system/services/service-party-app/electron/modules/tss-handler.ts index 1cef061a..422950be 100644 --- a/backend/mpc-system/services/service-party-app/electron/modules/tss-handler.ts +++ b/backend/mpc-system/services/service-party-app/electron/modules/tss-handler.ts @@ -477,6 +477,239 @@ export class TSSHandler extends EventEmitter { this.messageBuffer = []; } + // =========================================================================== + // Co-Sign 相关方法 - 与 Keygen 完全隔离的签名功能 + // =========================================================================== + + /** + * 预订阅签名消息流 - 在 joinSession 后立即调用 + * 与 prepareForKeygen 类似,确保在其他方开始发送消息时已准备好接收和缓冲 + * + * @param sessionId 签名会话 ID + * @param partyId 自己的 party ID + */ + prepareForSign(sessionId: string, partyId: string): void { + // 如果已经为同一个 session 准备过,跳过 + if (this.isPrepared && this.sessionId === sessionId) { + console.log('[TSS-SIGN] Already prepared for same session, skip'); + return; + } + + // 如果为不同的 session 准备过,先取消旧的订阅 + if (this.isPrepared && this.sessionId !== sessionId) { + console.log(`[TSS-SIGN] Switching from session ${this.sessionId?.substring(0, 8)}... to ${sessionId.substring(0, 8)}...`); + this.grpcClient.removeAllListeners('mpcMessage'); + this.grpcClient.unsubscribeMessages(); + this.messageBuffer = []; + } + + console.log(`[TSS-SIGN] Preparing for sign: session=${sessionId.substring(0, 8)}..., party=${partyId.substring(0, 8)}...`); + + this.sessionId = sessionId; + this.partyId = partyId; + this.isPrepared = true; + this.messageBuffer = []; + + // 立即订阅消息流,开始缓冲消息 + this.grpcClient.on('mpcMessage', this.handleIncomingMessage.bind(this)); + this.grpcClient.subscribeMessages(sessionId, partyId); + + console.log('[TSS-SIGN] Message subscription started, buffering enabled'); + } + + /** + * 取消签名预订阅 + */ + cancelSignPrepare(): void { + if (!this.isPrepared) { + return; + } + + console.log('[TSS-SIGN] Canceling sign prepare'); + this.isPrepared = false; + this.messageBuffer = []; + this.grpcClient.removeAllListeners('mpcMessage'); + this.grpcClient.unsubscribeMessages(); + this.sessionId = null; + this.partyId = null; + } + + /** + * 参与 Co-Sign 协议 + * + * @param sessionId 签名会话 ID + * @param partyId 自己的 party ID + * @param partyIndex 自己在签名参与方中的索引 + * @param participants 签名参与方列表 (T 个参与方) + * @param threshold 阈值配置 { t: 签名阈值, n: keygen时的总参与方数 } + * @param messageHash 待签名的消息哈希 (hex 编码) + * @param shareData 本地 share 数据 (base64 编码的加密数据) + * @param sharePassword share 解密密码 + */ + async participateSign( + sessionId: string, + partyId: string, + partyIndex: number, + participants: ParticipantInfo[], + threshold: { t: number; n: number }, + messageHash: string, + shareData: string, + sharePassword: string + ): Promise { + if (this.isRunning) { + throw new Error('TSS protocol already running'); + } + + // 检查是否已经预订阅 + const wasPrepared = this.isPrepared && this.sessionId === sessionId; + const bufferedCount = this.messageBuffer.length; + + console.log(`[TSS-SIGN] Starting sign: wasPrepared=${wasPrepared}, bufferedMessages=${bufferedCount}`); + + this.sessionId = sessionId; + this.partyId = partyId; + this.partyIndex = partyIndex; + this.participants = participants; + this.isRunning = true; + this.isProcessReady = false; + // 注意:不清空消息缓冲,保留预订阅阶段收到的消息 + + // 构建 party index map + this.partyIndexMap.clear(); + for (const p of participants) { + this.partyIndexMap.set(p.partyId, p.partyIndex); + } + + return new Promise((resolve, reject) => { + try { + const binaryPath = this.getTSSBinaryPath(); + console.log(`[TSS-SIGN] Binary path: ${binaryPath}`); + console.log(`[TSS-SIGN] Binary exists: ${fs.existsSync(binaryPath)}`); + + // 构建参与者列表 JSON + const participantsJson = JSON.stringify(participants); + console.log(`[TSS-SIGN] Participants: ${participantsJson}`); + console.log(`[TSS-SIGN] partyIndex=${partyIndex}, threshold=${threshold.t}-of-${threshold.n}`); + console.log(`[TSS-SIGN] messageHash=${messageHash.substring(0, 16)}...`); + + // 启动 TSS 子进程 - sign 命令 + const args = [ + 'sign', + '--session-id', sessionId, + '--party-id', partyId, + '--party-index', partyIndex.toString(), + '--threshold-t', threshold.t.toString(), + '--threshold-n', threshold.n.toString(), + '--participants', participantsJson, + '--message-hash', messageHash, + '--share-data', shareData, + '--password', sharePassword, + ]; + console.log(`[TSS-SIGN] Spawning: ${binaryPath} sign ...`); + + this.tssProcess = spawn(binaryPath, args); + + let resultData = ''; + let stderrData = ''; + + // 如果没有预订阅,现在订阅消息 + if (!wasPrepared) { + console.log('[TSS-SIGN] Subscribing to messages (not prepared before)'); + this.grpcClient.on('mpcMessage', this.handleIncomingMessage.bind(this)); + this.grpcClient.subscribeMessages(sessionId, partyId); + } else { + console.log(`[TSS-SIGN] Using existing subscription, ${bufferedCount} messages buffered`); + } + + // 处理标准输出 (JSON 消息) + this.tssProcess.stdout?.on('data', (data: Buffer) => { + const lines = data.toString().split('\n').filter(line => line.trim()); + + // 收到第一条输出时,标记进程就绪并发送缓冲的消息 + if (!this.isProcessReady && this.tssProcess?.stdin) { + this.isProcessReady = true; + console.log(`[TSS-SIGN] Process ready, flushing ${this.messageBuffer.length} buffered messages`); + this.flushMessageBuffer(); + } + + for (const line of lines) { + try { + const message: TSSMessage = JSON.parse(line); + this.handleTSSMessage(message); + + if (message.type === 'result') { + resultData = line; + } + } catch { + // 非 JSON 输出,记录日志 + console.log('[TSS-SIGN]', line); + } + } + }); + + // 处理标准错误 + this.tssProcess.stderr?.on('data', (data: Buffer) => { + const errorText = data.toString(); + stderrData += errorText; + console.error('[TSS-SIGN stderr]', errorText); + }); + + // 处理进程退出 + this.tssProcess.on('close', (code) => { + const completedSessionId = this.sessionId; + this.isRunning = false; + this.isProcessReady = false; + this.isPrepared = false; + this.messageBuffer = []; + this.tssProcess = null; + // 清理消息监听器 + this.grpcClient.removeAllListeners('mpcMessage'); + + if (code === 0 && resultData) { + try { + const result: TSSMessage = JSON.parse(resultData); + if (result.payload) { + // 成功完成后清理该会话的已处理消息记录 + if (this.database && completedSessionId) { + this.database.clearProcessedMessages(completedSessionId); + } + resolve({ + success: true, + signature: Buffer.from(result.payload, 'base64'), + }); + } else { + reject(new Error(result.error || 'Sign failed: no result data')); + } + } catch (e) { + reject(new Error(`Failed to parse sign result: ${e}`)); + } + } else { + const errorMsg = stderrData.trim() || `Sign process exited with code ${code}`; + console.error(`[TSS-SIGN] Process failed: code=${code}, stderr=${stderrData}`); + reject(new Error(errorMsg)); + } + }); + + // 处理进程错误 + this.tssProcess.on('error', (err) => { + this.isRunning = false; + this.isProcessReady = false; + this.isPrepared = false; + this.messageBuffer = []; + this.tssProcess = null; + // 清理消息监听器 + this.grpcClient.removeAllListeners('mpcMessage'); + reject(err); + }); + + } catch (err) { + this.isRunning = false; + this.isPrepared = false; + reject(err); + } + }); + } + /** * 取消正在进行的协议 */ diff --git a/backend/mpc-system/services/service-party-app/electron/preload.ts b/backend/mpc-system/services/service-party-app/electron/preload.ts index 811b73ef..a5e2da81 100644 --- a/backend/mpc-system/services/service-party-app/electron/preload.ts +++ b/backend/mpc-system/services/service-party-app/electron/preload.ts @@ -63,7 +63,7 @@ contextBridge.exposeInMainWorld('electronAPI', { register: (partyId: string, role: string) => ipcRenderer.invoke('grpc:register', { partyId, role }), - // 签名相关 + // 签名相关 (旧版 - persistent 签名使用) validateSigningSession: (code: string) => ipcRenderer.invoke('grpc:validateSigningSession', { code }), @@ -88,6 +88,53 @@ contextBridge.exposeInMainWorld('electronAPI', { }, }, + // =========================================================================== + // Co-Sign 相关 - 全新的多方协作签名 API + // =========================================================================== + cosign: { + // 创建 Co-Sign 会话 + createSession: (params: { + shareId: string; + sharePassword: string; + messageHash: string; + initiatorName?: string; + }) => ipcRenderer.invoke('cosign:createSession', params), + + // 验证邀请码 + validateInviteCode: (code: string) => + ipcRenderer.invoke('cosign:validateInviteCode', { code }), + + // 加入 Co-Sign 会话 + joinSession: (params: { + sessionId: string; + shareId: string; + sharePassword: string; + joinToken: string; + walletName?: string; + messageHash: string; + threshold: { t: number; n: number }; + }) => ipcRenderer.invoke('cosign:joinSession', params), + + // 获取会话状态 + getSessionStatus: (sessionId: string) => + ipcRenderer.invoke('cosign:getSessionStatus', { sessionId }), + + // 订阅会话事件 + subscribeSessionEvents: (sessionId: string, callback: (event: unknown) => void) => { + const channel = `cosign:events:${sessionId}`; + const listener = (_event: unknown, data: unknown) => callback(data); + eventSubscriptions.set(channel, listener); + ipcRenderer.on(channel, listener); + ipcRenderer.send('cosign:subscribeSessionEvents', { sessionId }); + + return () => { + ipcRenderer.removeListener(channel, listener); + eventSubscriptions.delete(channel); + ipcRenderer.send('cosign:unsubscribeSessionEvents', { sessionId }); + }; + }, + }, + // =========================================================================== // Account 服务相关 (HTTP API) // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/src/App.tsx b/backend/mpc-system/services/service-party-app/src/App.tsx index 0da8c865..18710f56 100644 --- a/backend/mpc-system/services/service-party-app/src/App.tsx +++ b/backend/mpc-system/services/service-party-app/src/App.tsx @@ -9,6 +9,10 @@ import Create from './pages/Create'; import Session from './pages/Session'; import Sign from './pages/Sign'; import Settings from './pages/Settings'; +// Co-Sign 页面 +import CoSignCreate from './pages/CoSignCreate'; +import CoSignJoin from './pages/CoSignJoin'; +import CoSignSession from './pages/CoSignSession'; function App() { const [startupComplete, setStartupComplete] = useState(false); @@ -37,12 +41,20 @@ function App() { } /> + {/* Keygen 路由 */} } /> } /> } /> } /> + {/* 旧版签名路由 (persistent) */} } /> } /> + {/* Co-Sign 路由 */} + } /> + } /> + } /> + } /> + {/* 设置 */} } /> } /> diff --git a/backend/mpc-system/services/service-party-app/src/pages/CoSignCreate.tsx b/backend/mpc-system/services/service-party-app/src/pages/CoSignCreate.tsx new file mode 100644 index 00000000..50a82a34 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/pages/CoSignCreate.tsx @@ -0,0 +1,265 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styles from './Create.module.css'; + +interface Share { + id: string; + walletName: string; + publicKey: string; + threshold: { t: number; n: number }; + createdAt: string; +} + +interface CreateCoSignResult { + success: boolean; + sessionId?: string; + inviteCode?: string; + error?: string; +} + +export default function CoSignCreate() { + const navigate = useNavigate(); + + const [shares, setShares] = useState([]); + const [selectedShareId, setSelectedShareId] = useState(''); + const [sharePassword, setSharePassword] = useState(''); + const [messageHash, setMessageHash] = useState(''); + const [participantName, setParticipantName] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [result, setResult] = useState(null); + const [step, setStep] = useState<'config' | 'creating' | 'created'>('config'); + + // 加载本地 shares + useEffect(() => { + const loadShares = async () => { + try { + const result = await window.electronAPI.storage.listShares(); + // 兼容不同返回格式 + const shareList = Array.isArray(result) ? result : ((result as any)?.data || []); + setShares(shareList); + if (shareList.length > 0) { + setSelectedShareId(shareList[0].id); + } + } catch (err) { + console.error('Failed to load shares:', err); + } + }; + loadShares(); + }, []); + + const handleCreateSession = async () => { + if (!selectedShareId) { + setError('请选择一个钱包'); + return; + } + if (!messageHash.trim()) { + setError('请输入待签名的消息哈希'); + return; + } + if (!/^[0-9a-fA-F]{64}$/.test(messageHash.trim())) { + setError('消息哈希必须是 64 位十六进制字符串 (32 字节)'); + return; + } + if (!participantName.trim()) { + setParticipantName('发起者'); + } + + setStep('creating'); + setIsLoading(true); + setError(null); + + try { + const createResult = await window.electronAPI.cosign.createSession({ + shareId: selectedShareId, + sharePassword: sharePassword, + messageHash: messageHash.trim().toLowerCase(), + initiatorName: participantName.trim() || '发起者', + }); + + if (createResult.success) { + setResult(createResult); + setStep('created'); + } else { + setError(createResult.error || '创建签名会话失败'); + setStep('config'); + } + } catch (err) { + setError('创建签名会话失败,请检查网络连接'); + setStep('config'); + } finally { + setIsLoading(false); + } + }; + + const handleCopyInviteCode = async () => { + if (result?.inviteCode) { + try { + await navigator.clipboard.writeText(result.inviteCode); + } catch (err) { + console.error('Failed to copy:', err); + } + } + }; + + const handleGoToSession = () => { + if (result?.sessionId) { + navigate(`/cosign/session/${result.sessionId}`); + } + }; + + const selectedShare = shares.find(s => s.id === selectedShareId); + + return ( +
+
+

发起多方签名

+

选择钱包并输入待签名的消息哈希

+ + {step === 'config' && ( +
+ {/* 签名说明 */} +
+
i
+
+ 多方协作签名说明 +
    +
  • 选择钱包: 使用已创建的共管钱包进行签名
  • +
  • 消息哈希: 待签名数据的 SHA256 哈希值
  • +
  • 阈值签名: 需要足够数量的参与方共同完成
  • +
  • 安全性: 私钥份额永不离开本地设备
  • +
+
+
+ + {/* 选择钱包 */} +
+ + {shares.length === 0 ? ( +

暂无可用钱包,请先创建或加入共管钱包

+ ) : ( + + )} + {selectedShare && ( +

+ 公钥: {selectedShare.publicKey.substring(0, 16)}... +

+ )} +
+ + {/* 钱包密码 */} +
+ + setSharePassword(e.target.value)} + placeholder="如果设置了密码,请输入" + className={styles.input} + disabled={isLoading} + /> +
+ + {/* 消息哈希 */} +
+ + setMessageHash(e.target.value)} + placeholder="64位十六进制字符串,如: a1b2c3d4..." + className={styles.input} + disabled={isLoading} + /> +

+ 待签名数据的 SHA256 哈希值 (32 字节 = 64 个十六进制字符) +

+
+ + {/* 参与者名称 */} +
+ + setParticipantName(e.target.value)} + placeholder="输入您的名称(其他参与者可见)" + className={styles.input} + disabled={isLoading} + /> +
+ + {error &&
{error}
} + +
+ + +
+
+ )} + + {step === 'creating' && ( +
+
+

正在创建签名会话...

+
+ )} + + {step === 'created' && result && ( +
+
OK
+

签名会话创建成功

+ +
+ +
+ {result.inviteCode} + +
+

+ 将此邀请码分享给其他参与方,他们可以使用此码加入签名 +

+
+ +
+ +
+
+ )} +
+
+ ); +} diff --git a/backend/mpc-system/services/service-party-app/src/pages/CoSignJoin.tsx b/backend/mpc-system/services/service-party-app/src/pages/CoSignJoin.tsx new file mode 100644 index 00000000..6c1a63e8 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/pages/CoSignJoin.tsx @@ -0,0 +1,343 @@ +import { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import styles from './Join.module.css'; + +interface Share { + id: string; + walletName: string; + publicKey: string; + sessionId: string; + threshold: { t: number; n: number }; +} + +interface SessionInfo { + sessionId: string; + keygenSessionId: string; + walletName: string; + messageHash: string; + threshold: { t: number; n: number }; + status: string; + currentParticipants: number; +} + +interface ValidateResult { + success: boolean; + error?: string; + sessionInfo?: SessionInfo; + joinToken?: string; +} + +export default function CoSignJoin() { + const { inviteCode } = useParams<{ inviteCode?: string }>(); + const navigate = useNavigate(); + + const [code, setCode] = useState(inviteCode || ''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [sessionInfo, setSessionInfo] = useState(null); + const [joinToken, setJoinToken] = useState(null); + const [step, setStep] = useState<'input' | 'select_share' | 'joining'>('input'); + + // Share 选择相关 + const [shares, setShares] = useState([]); + const [selectedShareId, setSelectedShareId] = useState(''); + const [sharePassword, setSharePassword] = useState(''); + const [autoJoinAttempted, setAutoJoinAttempted] = useState(false); + + // 加载本地 shares + useEffect(() => { + const loadShares = async () => { + try { + const result = await window.electronAPI.storage.listShares(); + // 兼容不同返回格式 + const shareList = Array.isArray(result) ? result : ((result as any)?.data || []); + setShares(shareList); + } catch (err) { + console.error('Failed to load shares:', err); + } + }; + loadShares(); + }, []); + + // 如果 URL 中有邀请码,自动验证 + useEffect(() => { + if (inviteCode) { + handleValidateCode(inviteCode); + } + }, [inviteCode]); + + // 自动选择匹配的 share + useEffect(() => { + if (sessionInfo && shares.length > 0 && !selectedShareId) { + // 尝试找到匹配的 share(基于 keygen session ID) + const matchingShare = shares.find(s => s.sessionId === sessionInfo.keygenSessionId); + if (matchingShare) { + setSelectedShareId(matchingShare.id); + } else if (shares.length === 1) { + // 如果只有一个 share,自动选择 + setSelectedShareId(shares[0].id); + } + } + }, [sessionInfo, shares, selectedShareId]); + + // 自动加入 + useEffect(() => { + if ( + step === 'select_share' && + sessionInfo && + joinToken && + selectedShareId && + !autoJoinAttempted && + !isLoading + ) { + // 找到匹配的 share 且未尝试过自动加入,则自动加入 + const matchingShare = shares.find(s => s.sessionId === sessionInfo.keygenSessionId); + if (matchingShare && matchingShare.id === selectedShareId) { + setAutoJoinAttempted(true); + handleJoinSession(); + } + } + }, [step, sessionInfo, joinToken, selectedShareId, autoJoinAttempted, isLoading, shares]); + + const handleValidateCode = async (codeToValidate: string) => { + if (!codeToValidate.trim()) { + setError('请输入邀请码'); + return; + } + + setIsLoading(true); + setError(null); + + try { + const result: ValidateResult = await window.electronAPI.cosign.validateInviteCode(codeToValidate); + if (result.success && result.sessionInfo) { + setSessionInfo(result.sessionInfo); + if (result.joinToken) { + setJoinToken(result.joinToken); + } + setStep('select_share'); + } else { + setError(result.error || '无效的邀请码'); + } + } catch (err) { + setError('验证邀请码失败,请检查网络连接'); + } finally { + setIsLoading(false); + } + }; + + const handleJoinSession = async () => { + if (!sessionInfo) { + setError('会话信息不完整'); + return; + } + + if (!joinToken) { + setError('未获取到加入令牌,请重新验证邀请码'); + return; + } + + if (!selectedShareId) { + setError('请选择一个钱包'); + return; + } + + setStep('joining'); + setIsLoading(true); + setError(null); + + try { + const result = await window.electronAPI.cosign.joinSession({ + sessionId: sessionInfo.sessionId, + shareId: selectedShareId, + sharePassword: sharePassword, + joinToken: joinToken, + walletName: sessionInfo.walletName, + messageHash: sessionInfo.messageHash, + threshold: sessionInfo.threshold, + }); + + if (result.success) { + navigate(`/cosign/session/${sessionInfo.sessionId}`); + } else { + setError(result.error || '加入会话失败'); + setStep('select_share'); + } + } catch (err) { + setError('加入会话失败,请重试'); + setStep('select_share'); + } finally { + setIsLoading(false); + } + }; + + const handlePaste = async () => { + try { + const text = await navigator.clipboard.readText(); + setCode(text.trim()); + } catch (err) { + console.error('Failed to read clipboard:', err); + } + }; + + const selectedShare = shares.find(s => s.id === selectedShareId); + + return ( +
+
+

加入多方签名

+

输入邀请码加入签名会话

+ + {step === 'input' && ( +
+
+ +
+ setCode(e.target.value)} + placeholder="粘贴签名邀请码" + className={styles.input} + disabled={isLoading} + /> + +
+
+ + {error &&
{error}
} + +
+ + +
+
+ )} + + {step === 'select_share' && sessionInfo && ( +
+
+

签名会话信息

+
+
+ 钱包名称 + {sessionInfo.walletName} +
+
+ 签名阈值 + + {sessionInfo.threshold.t}-of-{sessionInfo.threshold.n} + +
+
+ 消息哈希 + + {sessionInfo.messageHash.substring(0, 16)}... + +
+
+ 当前参与者 + + {sessionInfo.currentParticipants} / {sessionInfo.threshold.t} + +
+
+
+ + {/* 选择本地 share */} +
+ + {shares.length === 0 ? ( +

暂无可用钱包,请先创建或加入共管钱包

+ ) : ( + + )} + {selectedShare && ( +

+ 公钥: {selectedShare.publicKey.substring(0, 16)}... +

+ )} +
+ + {/* 钱包密码 */} +
+ + setSharePassword(e.target.value)} + placeholder="如果设置了密码,请输入" + className={styles.input} + disabled={isLoading} + /> +
+ + {error &&
{error}
} + +
+ + +
+
+ )} + + {step === 'joining' && ( +
+
+

正在加入签名会话...

+
+ )} +
+
+ ); +} diff --git a/backend/mpc-system/services/service-party-app/src/pages/CoSignSession.tsx b/backend/mpc-system/services/service-party-app/src/pages/CoSignSession.tsx new file mode 100644 index 00000000..11c01a78 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/pages/CoSignSession.tsx @@ -0,0 +1,329 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import styles from './Session.module.css'; + +interface Participant { + partyId: string; + name: string; + status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed'; +} + +interface SessionState { + sessionId: string; + walletName: string; + messageHash: string; + threshold: { t: number; n: number }; + status: 'waiting' | 'ready' | 'processing' | 'completed' | 'failed'; + participants: Participant[]; + currentRound: number; + totalRounds: number; + signature?: string; + error?: string; +} + +export default function CoSignSession() { + const { sessionId } = useParams<{ sessionId: string }>(); + const navigate = useNavigate(); + + const [session, setSession] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchSessionStatus = useCallback(async () => { + if (!sessionId) return; + + try { + const result = await window.electronAPI.cosign.getSessionStatus(sessionId); + if (result.success && result.session) { + setSession({ + sessionId: result.session.sessionId || sessionId, + walletName: result.session.walletName || '', + messageHash: result.session.messageHash || '', + threshold: result.session.threshold || { t: 0, n: 0 }, + status: mapStatus(result.session.status), + participants: result.session.participants || [], + currentRound: 0, + totalRounds: 9, // GG20 签名有 9 轮 + }); + } else { + setError(result.error || '获取会话状态失败'); + } + } catch (err) { + setError('获取会话状态失败'); + } finally { + setIsLoading(false); + } + }, [sessionId]); + + // 映射后端状态到前端状态 + const mapStatus = (status: string): 'waiting' | 'ready' | 'processing' | 'completed' | 'failed' => { + switch (status) { + case 'pending': + case 'waiting': + return 'waiting'; + case 'all_joined': + case 'ready': + return 'ready'; + case 'in_progress': + case 'signing': + return 'processing'; + case 'completed': + return 'completed'; + case 'failed': + case 'expired': + return 'failed'; + default: + return 'waiting'; + } + }; + + useEffect(() => { + fetchSessionStatus(); + + // 订阅会话事件 + const unsubscribe = window.electronAPI.cosign.subscribeSessionEvents( + sessionId!, + (event: any) => { + console.log('[CoSignSession] Received event:', event); + + if (event.type === 'participant_joined') { + setSession(prev => prev ? { + ...prev, + participants: event.participant + ? [...prev.participants, event.participant] + : prev.participants, + } : null); + // 刷新状态 + fetchSessionStatus(); + } else if (event.type === 'status_changed' || event.type === 'all_joined') { + setSession(prev => prev ? { + ...prev, + status: event.status ? mapStatus(event.status) : prev.status, + } : null); + // 刷新状态 + fetchSessionStatus(); + } else if (event.type === 'progress') { + setSession(prev => prev ? { + ...prev, + status: 'processing', + currentRound: event.round || prev.currentRound, + totalRounds: event.totalRounds || prev.totalRounds, + } : null); + } else if (event.type === 'completed') { + setSession(prev => prev ? { + ...prev, + status: 'completed', + signature: event.signature, + } : null); + } else if (event.type === 'failed' || event.type === 'sign_start_timeout') { + setSession(prev => prev ? { + ...prev, + status: 'failed', + error: event.error, + } : null); + } + } + ); + + return () => { + unsubscribe(); + }; + }, [sessionId, fetchSessionStatus]); + + const getStatusText = (status: string) => { + switch (status) { + case 'waiting': return '等待参与方'; + case 'ready': return '准备就绪'; + case 'processing': return '签名进行中'; + case 'completed': return '签名完成'; + case 'failed': return '签名失败'; + default: return status; + } + }; + + const getStatusClass = (status: string) => { + switch (status) { + case 'waiting': return styles.statusWaiting; + case 'ready': return styles.statusReady; + case 'processing': return styles.statusProcessing; + case 'completed': return styles.statusCompleted; + case 'failed': return styles.statusFailed; + default: return ''; + } + }; + + const getParticipantStatusIcon = (status: string) => { + switch (status) { + case 'waiting': return '...'; + case 'ready': return 'OK'; + case 'processing': return '*'; + case 'completed': return 'OK'; + case 'failed': return 'X'; + default: return '-'; + } + }; + + const handleCopySignature = async () => { + if (session?.signature) { + try { + await navigator.clipboard.writeText(session.signature); + } catch (err) { + console.error('Failed to copy:', err); + } + } + }; + + if (isLoading) { + return ( +
+
+
+

加载签名会话信息...

+
+
+ ); + } + + if (error || !session) { + return ( +
+
+
!
+

加载失败

+

{error || '无法获取会话信息'}

+ +
+
+ ); + } + + return ( +
+
+
+
+

{session.walletName || '多方签名'}

+

会话 ID: {session.sessionId?.substring(0, 16)}...

+
+ + {getStatusText(session.status)} + +
+ +
+ {/* 消息哈希 */} + {session.messageHash && ( +
+

待签名消息

+ + {session.messageHash} + +
+ )} + + {/* 进度部分 */} + {session.status === 'processing' && ( +
+
+ 签名进度 + {session.currentRound} / {session.totalRounds} +
+
+
+
+
+ )} + + {/* 参与方列表 */} +
+

+ 参与方 ({(session.participants || []).length} / {session.threshold?.t || 0}) +

+
+ {(session.participants || []).map((participant, index) => ( +
+
+ #{index + 1} + {participant.name || `参与方 ${index + 1}`} +
+ + {getParticipantStatusIcon(participant.status)} + +
+ ))} + {Array.from({ length: Math.max(0, (session.threshold?.t || 0) - (session.participants || []).length) }).map((_, index) => ( +
+
+ #{(session.participants || []).length + index + 1} + 等待加入... +
+ ... +
+ ))} +
+
+ + {/* 阈值信息 */} + {session.threshold && ( +
+

签名阈值

+
+ + {session.threshold.t}-of-{session.threshold.n} + + + 需要 {session.threshold.t} 个参与方共同签名 + +
+
+ )} + + {/* 完成状态 */} + {session.status === 'completed' && session.signature && ( +
+

签名结果

+
+ {session.signature} + +
+

+ OK 签名已成功生成 +

+
+ )} + + {/* 失败状态 */} + {session.status === 'failed' && session.error && ( +
+
+ ! + {session.error} +
+
+ )} +
+ +
+ +
+
+
+ ); +} diff --git a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts index 5e60dac1..fe5fc64d 100644 --- a/backend/mpc-system/services/service-party-app/src/types/electron.d.ts +++ b/backend/mpc-system/services/service-party-app/src/types/electron.d.ts @@ -410,6 +410,93 @@ interface KavaHealthCheckResult { // Electron API 接口 // =========================================================================== +// =========================================================================== +// Co-Sign 相关类型 +// =========================================================================== + +interface CoSignSessionInfo { + sessionId: string; + keygenSessionId: string; + walletName: string; + messageHash: string; + threshold: { t: number; n: number }; + status: string; + currentParticipants: number; + parties?: Array<{ party_id: string; party_index: number }>; +} + +interface CreateCoSignSessionParams { + shareId: string; + sharePassword: string; + messageHash: string; + initiatorName?: string; +} + +interface CreateCoSignSessionResult { + success: boolean; + sessionId?: string; + inviteCode?: string; + walletName?: string; + expiresAt?: string; + error?: string; +} + +interface ValidateCoSignInviteCodeResult { + success: boolean; + sessionInfo?: CoSignSessionInfo; + joinToken?: string; + error?: string; +} + +interface JoinCoSignSessionParams { + sessionId: string; + shareId: string; + sharePassword: string; + joinToken: string; + walletName?: string; + messageHash: string; + threshold: { t: number; n: number }; +} + +interface JoinCoSignSessionResult { + success: boolean; + data?: unknown; + error?: string; +} + +interface GetCoSignSessionStatusResult { + success: boolean; + session?: { + sessionId: string; + status: string; + joinedCount: number; + threshold: { t: number; n: number }; + participants: Array<{ + partyId: string; + partyIndex: number; + name: string; + status: string; + }>; + messageHash: string; + walletName: string; + }; + error?: string; +} + +interface CoSignSessionEvent { + type: 'participant_joined' | 'status_changed' | 'all_joined' | 'progress' | 'completed' | 'failed' | 'sign_start_timeout'; + participant?: { + partyId: string; + name: string; + status: string; + }; + status?: string; + round?: number; + totalRounds?: number; + signature?: string; + error?: string; +} + interface ElectronAPI { // Account 服务相关 (HTTP API) account: { @@ -521,6 +608,15 @@ interface ElectronAPI { unsubscribeLogs: () => void; log: (level: string, source: string, message: string) => void; }; + + // Co-Sign 相关 (多方协作签名) + cosign: { + createSession: (params: CreateCoSignSessionParams) => Promise; + validateInviteCode: (code: string) => Promise; + joinSession: (params: JoinCoSignSessionParams) => Promise; + getSessionStatus: (sessionId: string) => Promise; + subscribeSessionEvents: (sessionId: string, callback: (event: CoSignSessionEvent) => void) => () => void; + }; } declare global { diff --git a/backend/mpc-system/services/service-party-app/tss-party/go.mod b/backend/mpc-system/services/service-party-app/tss-party/go.mod index df9dd7c0..51afe69a 100644 --- a/backend/mpc-system/services/service-party-app/tss-party/go.mod +++ b/backend/mpc-system/services/service-party-app/tss-party/go.mod @@ -6,7 +6,10 @@ require github.com/bnb-chain/tss-lib/v2 v2.0.2 require ( github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 // indirect + github.com/btcsuite/btcd v0.23.4 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/btcsuite/btcutil v1.0.2 // indirect github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -20,6 +23,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.13.0 // indirect golang.org/x/sys v0.15.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/backend/mpc-system/services/service-party-app/tss-party/go.sum b/backend/mpc-system/services/service-party-app/tss-party/go.sum index 3318fd86..301c5521 100644 --- a/backend/mpc-system/services/service-party-app/tss-party/go.sum +++ b/backend/mpc-system/services/service-party-app/tss-party/go.sum @@ -7,6 +7,7 @@ github.com/bnb-chain/tss-lib/v2 v2.0.2 h1:dL2GJFCSYsYQ0bHkGll+hNM2JWsC1rxDmJJJQE github.com/bnb-chain/tss-lib/v2 v2.0.2/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.4 h1:IzV6qqkfwbItOS/sg/aDfPDsjPP8twrCOE2R93hxMlQ= github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= @@ -15,9 +16,11 @@ github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= @@ -144,6 +147,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= diff --git a/backend/mpc-system/services/service-party-app/tss-party/integration_test.go b/backend/mpc-system/services/service-party-app/tss-party/integration_test.go new file mode 100644 index 00000000..8948d151 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/tss-party/integration_test.go @@ -0,0 +1,674 @@ +// integration_test.go - Integration test for the complete co-sign flow +// Tests: session creation, joining, waiting, events, and signing +package main + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math/big" + "net/http" + "os" + "os/exec" + "sync" + "testing" + "time" + + "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" + "github.com/bnb-chain/tss-lib/v2/tss" +) + +const ( + // Backend service URLs (from docker-compose.windows.yml) + accountServiceURL = "http://localhost:4000" + grpcRouterAddr = "localhost:50051" +) + +// API Response types for co-managed keygen +type CreateCoManagedSessionRequest struct { + WalletName string `json:"wallet_name"` + ThresholdT int `json:"threshold_t"` + ThresholdN int `json:"threshold_n"` + InitiatorPartyID string `json:"initiator_party_id"` + InitiatorName string `json:"initiator_name,omitempty"` + PersistentCount int `json:"persistent_count"` +} + +type CreateCoManagedSessionResponse struct { + SessionID string `json:"session_id"` + InviteCode string `json:"invite_code"` + WalletName string `json:"wallet_name"` + ThresholdT int `json:"threshold_t"` + ThresholdN int `json:"threshold_n"` + ExpiresAt int64 `json:"expires_at"` + JoinToken string `json:"join_token"` + PartyID string `json:"party_id"` + PartyIndex int `json:"party_index"` +} + +// API Response types for co-managed sign +type CreateSignSessionRequest struct { + KeygenSessionID string `json:"keygen_session_id"` + WalletName string `json:"wallet_name"` + MessageHash string `json:"message_hash"` + Parties []SignPartyInfo `json:"parties"` + ThresholdT int `json:"threshold_t"` + InitiatorName string `json:"initiator_name,omitempty"` +} + +type CreateSignSessionResponse struct { + SessionID string `json:"session_id"` + InviteCode string `json:"invite_code"` + KeygenSessionID string `json:"keygen_session_id"` + MessageHash string `json:"message_hash"` + ThresholdT int `json:"threshold_t"` + ExpiresAt int64 `json:"expires_at"` + JoinToken string `json:"join_token"` +} + +type GetSignSessionResponse struct { + SessionID string `json:"session_id"` + KeygenSessionID string `json:"keygen_session_id"` + WalletName string `json:"wallet_name"` + MessageHash string `json:"message_hash"` + ThresholdT int `json:"threshold_t"` + Status string `json:"status"` + InviteCode string `json:"invite_code"` + ExpiresAt int64 `json:"expires_at"` + Parties []SignPartyInfo `json:"parties"` + JoinedCount int `json:"joined_count"` + JoinToken string `json:"join_token,omitempty"` +} + +type SignPartyInfo struct { + PartyID string `json:"party_id"` + PartyIndex int `json:"party_index"` +} + +// TestAccountServiceHealth tests if account service is available +func TestAccountServiceHealth(t *testing.T) { + resp, err := http.Get(accountServiceURL + "/health") + if err != nil { + t.Fatalf("Failed to connect to account service: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + t.Fatalf("Account service unhealthy, status: %d", resp.StatusCode) + } + + t.Log("Account service is healthy") +} + +// TestCreateSignSession tests creating a new sign session +func TestCreateSignSession(t *testing.T) { + // This requires an existing keygen session ID + // For testing, we'll use a mock one + keygenSessionID := "test-keygen-session-" + fmt.Sprintf("%d", time.Now().UnixNano()) + messageHash := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + + reqBody := CreateSignSessionRequest{ + KeygenSessionID: keygenSessionID, + MessageHash: messageHash, + InitiatorName: "test-initiator", + } + + jsonBody, _ := json.Marshal(reqBody) + resp, err := http.Post( + accountServiceURL+"/api/v1/co-managed/sign", + "application/json", + bytes.NewBuffer(jsonBody), + ) + if err != nil { + t.Fatalf("Failed to create sign session: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + t.Logf("Create session response (status %d): %s", resp.StatusCode, string(body)) + + if resp.StatusCode != 200 && resp.StatusCode != 201 { + t.Logf("Note: This test requires an existing keygen session in the database") + t.Skipf("Sign session creation returned status %d (expected existing keygen session)", resp.StatusCode) + } + + var result CreateSignSessionResponse + if err := json.Unmarshal(body, &result); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + t.Logf("Session created: ID=%s, InviteCode=%s", result.SessionID, result.InviteCode) +} + +// TestGetSessionByInviteCode tests retrieving session info by invite code +func TestGetSessionByInviteCode(t *testing.T) { + // This would need a valid invite code from a real session + inviteCode := "TEST123" + + resp, err := http.Get(accountServiceURL + "/api/v1/co-managed/sign/by-invite-code/" + inviteCode) + if err != nil { + t.Fatalf("Failed to get session: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + t.Logf("Get session response (status %d): %s", resp.StatusCode, string(body)) + + if resp.StatusCode == 404 { + t.Skip("No session found with invite code (expected for test)") + } +} + +// TestCoManagedKeygenSessionFlow tests the keygen session creation and join flow +func TestCoManagedKeygenSessionFlow(t *testing.T) { + t.Log("=== Co-Managed Keygen Session Flow Test ===") + + // Step 1: Create a keygen session + t.Log("Step 1: Creating keygen session...") + reqBody := CreateCoManagedSessionRequest{ + WalletName: "test-wallet-" + fmt.Sprintf("%d", time.Now().UnixNano()), + ThresholdT: 1, // 2-of-3: t=1 means t+1=2 signers needed + ThresholdN: 3, + InitiatorPartyID: "test-initiator-party", + InitiatorName: "Test User", + PersistentCount: 0, // No server parties for this test + } + + jsonBody, _ := json.Marshal(reqBody) + resp, err := http.Post( + accountServiceURL+"/api/v1/co-managed/sessions", + "application/json", + bytes.NewBuffer(jsonBody), + ) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + t.Logf("Create session response (status %d): %s", resp.StatusCode, string(body)) + + if resp.StatusCode != 200 && resp.StatusCode != 201 { + t.Fatalf("Failed to create keygen session, status: %d", resp.StatusCode) + } + + var createResp CreateCoManagedSessionResponse + if err := json.Unmarshal(body, &createResp); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + t.Logf("Session created: ID=%s, InviteCode=%s", createResp.SessionID, createResp.InviteCode) + + // Step 2: Get session by invite code + t.Log("Step 2: Getting session by invite code...") + resp2, err := http.Get(accountServiceURL + "/api/v1/co-managed/sessions/by-invite-code/" + createResp.InviteCode) + if err != nil { + t.Fatalf("Failed to get session: %v", err) + } + defer resp2.Body.Close() + + body2, _ := io.ReadAll(resp2.Body) + t.Logf("Get session response (status %d): %s", resp2.StatusCode, string(body2)) + + if resp2.StatusCode != 200 { + t.Fatalf("Failed to get session by invite code, status: %d", resp2.StatusCode) + } + + // Step 3: Get session status + t.Log("Step 3: Getting session status...") + resp3, err := http.Get(accountServiceURL + "/api/v1/co-managed/sessions/" + createResp.SessionID) + if err != nil { + t.Fatalf("Failed to get session status: %v", err) + } + defer resp3.Body.Close() + + body3, _ := io.ReadAll(resp3.Body) + t.Logf("Session status response (status %d): %s", resp3.StatusCode, string(body3)) + + if resp3.StatusCode != 200 { + t.Fatalf("Failed to get session status, status: %d", resp3.StatusCode) + } + + t.Log("Keygen session flow test passed!") +} + +// TestCoManagedSignSessionFlow tests the full sign session flow +func TestCoManagedSignSessionFlow(t *testing.T) { + t.Log("=== Co-Managed Sign Session Flow Test ===") + + // First, create a keygen session to get a valid keygen_session_id + t.Log("Step 1: Creating keygen session...") + keygenReq := CreateCoManagedSessionRequest{ + WalletName: "test-wallet-for-sign-" + fmt.Sprintf("%d", time.Now().UnixNano()), + ThresholdT: 1, + ThresholdN: 3, + InitiatorPartyID: "test-party-0", + InitiatorName: "Test User", + PersistentCount: 0, + } + + jsonBody, _ := json.Marshal(keygenReq) + resp, err := http.Post( + accountServiceURL+"/api/v1/co-managed/sessions", + "application/json", + bytes.NewBuffer(jsonBody), + ) + if err != nil { + t.Fatalf("Failed to create keygen session: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + t.Logf("Keygen session response (status %d): %s", resp.StatusCode, string(body)) + + if resp.StatusCode != 200 && resp.StatusCode != 201 { + t.Fatalf("Failed to create keygen session, status: %d", resp.StatusCode) + } + + var keygenResp CreateCoManagedSessionResponse + if err := json.Unmarshal(body, &keygenResp); err != nil { + t.Fatalf("Failed to parse keygen response: %v", err) + } + + // Step 2: Create a sign session using the keygen session ID + // Note: For threshold T, we need T+1 parties to sign + // Backend validates that parties.length >= threshold_t + 1 + t.Log("Step 2: Creating sign session...") + signReq := CreateSignSessionRequest{ + KeygenSessionID: keygenResp.SessionID, + WalletName: keygenReq.WalletName, + MessageHash: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + ThresholdT: 1, // Threshold T=1 means T+1=2 parties needed to sign + Parties: []SignPartyInfo{ + {PartyID: "test-party-0", PartyIndex: 0}, + {PartyID: "test-party-1", PartyIndex: 1}, + }, + InitiatorName: "Test User", + } + + jsonBody2, _ := json.Marshal(signReq) + resp2, err := http.Post( + accountServiceURL+"/api/v1/co-managed/sign", + "application/json", + bytes.NewBuffer(jsonBody2), + ) + if err != nil { + t.Fatalf("Failed to create sign session: %v", err) + } + defer resp2.Body.Close() + + body2, _ := io.ReadAll(resp2.Body) + t.Logf("Sign session response (status %d): %s", resp2.StatusCode, string(body2)) + + if resp2.StatusCode != 200 && resp2.StatusCode != 201 { + t.Fatalf("Failed to create sign session, status: %d", resp2.StatusCode) + } + + var signResp CreateSignSessionResponse + if err := json.Unmarshal(body2, &signResp); err != nil { + t.Fatalf("Failed to parse sign response: %v", err) + } + + t.Logf("Sign session created: ID=%s, InviteCode=%s", signResp.SessionID, signResp.InviteCode) + + // Step 3: Get sign session by invite code + t.Log("Step 3: Getting sign session by invite code...") + resp3, err := http.Get(accountServiceURL + "/api/v1/co-managed/sign/by-invite-code/" + signResp.InviteCode) + if err != nil { + t.Fatalf("Failed to get sign session: %v", err) + } + defer resp3.Body.Close() + + body3, _ := io.ReadAll(resp3.Body) + t.Logf("Get sign session response (status %d): %s", resp3.StatusCode, string(body3)) + + if resp3.StatusCode != 200 { + t.Fatalf("Failed to get sign session by invite code, status: %d", resp3.StatusCode) + } + + var getSignResp GetSignSessionResponse + if err := json.Unmarshal(body3, &getSignResp); err != nil { + t.Fatalf("Failed to parse get sign session response: %v", err) + } + + t.Logf("Sign session status: %s, JoinedCount: %d, ThresholdT: %d", + getSignResp.Status, getSignResp.JoinedCount, getSignResp.ThresholdT) + + t.Log("Sign session flow test passed!") +} + +// TestFullSignFlow tests the complete signing flow with mock data +func TestFullSignFlow(t *testing.T) { + t.Log("=== Full Co-Sign Flow Test ===") + + // Step 1: Generate mock key shares (simulating keygen result) + // NOTE: In tss-lib, threshold parameter means "t" where you need t+1 parties to sign. + // So for 2-of-3, we use threshold=1 (meaning 1+1=2 parties needed to sign) + t.Log("Step 1: Generating mock key shares for 2-of-3 scheme...") + thresholdT := 1 // t value: need t+1=2 parties to sign + totalN := 3 // total parties + shares, err := generateMockKeyShares(thresholdT, totalN) + if err != nil { + t.Fatalf("Failed to generate key shares: %v", err) + } + t.Logf("Generated %d key shares", len(shares)) + + // Step 2: Prepare signing parameters + messageHash := "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + sessionID := fmt.Sprintf("test-sign-session-%d", time.Now().UnixNano()) + password := "test-password" + + // Step 3: Start signing with 2 parties (index 0 and 1) + // For tss-party.exe, we pass the signing threshold (t+1=2) and total (n=3) + t.Log("Step 2: Starting sign process with 2 parties...") + signingParties := []int{0, 1} + + // Pass thresholdT+1=2 as the signing threshold to tss-party.exe + signature, err := runSigningProcess(shares, signingParties, messageHash, sessionID, password, thresholdT+1, totalN, t) + if err != nil { + t.Fatalf("Signing failed: %v", err) + } + + t.Logf("Step 3: Signing complete!") + t.Logf("Signature (hex): %x", signature) + t.Logf("Signature (base64): %s", base64.StdEncoding.EncodeToString(signature)) +} + +// generateMockKeyShares generates key shares using tss-lib directly +func generateMockKeyShares(threshold, total int) ([]*keygen.LocalPartySaveData, error) { + fmt.Println("[Keygen] Starting key generation for", total, "parties with threshold", threshold) + + partyIDs := make([]*tss.PartyID, total) + for i := 0; i < total; i++ { + partyIDs[i] = tss.NewPartyID( + fmt.Sprintf("party-%d", i), + fmt.Sprintf("party-%d", i), + big.NewInt(int64(i+1)), + ) + } + + sortedPartyIDs := tss.SortPartyIDs(partyIDs) + peerCtx := tss.NewPeerContext(sortedPartyIDs) + + outChannels := make([]chan tss.Message, total) + endChannels := make([]chan *keygen.LocalPartySaveData, total) + parties := make([]tss.Party, total) + + for i := 0; i < total; i++ { + outChannels[i] = make(chan tss.Message, total*20) + endChannels[i] = make(chan *keygen.LocalPartySaveData, 1) + params := tss.NewParameters(tss.S256(), peerCtx, sortedPartyIDs[i], total, threshold) + parties[i] = keygen.NewLocalParty(params, outChannels[i], endChannels[i]) + } + + // Start all parties + var wg sync.WaitGroup + errChan := make(chan error, total) + + for i := 0; i < total; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + if err := parties[idx].Start(); err != nil { + errChan <- fmt.Errorf("party %d start error: %w", idx, err) + } + }(i) + } + + results := make([]*keygen.LocalPartySaveData, total) + var resultsMu sync.Mutex + resultCount := 0 + + done := make(chan struct{}) + + // Message routing + go func() { + for { + select { + case <-done: + return + default: + for i := 0; i < total; i++ { + select { + case msg := <-outChannels[i]: + wire, _, _ := msg.WireBytes() + if msg.IsBroadcast() { + for j := 0; j < total; j++ { + if j != i { + go func(destIdx int) { + parsed, _ := tss.ParseWireMessage(wire, msg.GetFrom(), true) + parties[destIdx].Update(parsed) + }(j) + } + } + } else { + for _, to := range msg.GetTo() { + for j := 0; j < total; j++ { + if sortedPartyIDs[j].Id == to.Id { + go func(destIdx int) { + parsed, _ := tss.ParseWireMessage(wire, msg.GetFrom(), false) + parties[destIdx].Update(parsed) + }(j) + break + } + } + } + } + case result := <-endChannels[i]: + resultsMu.Lock() + results[i] = result + resultCount++ + fmt.Printf("[Keygen] Party %d completed\n", i) + if resultCount == total { + close(done) + } + resultsMu.Unlock() + default: + } + } + time.Sleep(5 * time.Millisecond) + } + } + }() + + wg.Wait() + + select { + case <-done: + fmt.Println("[Keygen] All parties completed successfully") + case <-time.After(5 * time.Minute): + return nil, fmt.Errorf("keygen timeout") + } + + select { + case err := <-errChan: + return nil, err + default: + } + + return results, nil +} + +// runSigningProcess runs the signing process using tss-party.exe +func runSigningProcess( + shares []*keygen.LocalPartySaveData, + signingPartyIndices []int, + messageHash, sessionID, password string, + thresholdT, thresholdN int, + t *testing.T, +) ([]byte, error) { + // Build participants + participants := make([]Participant, len(signingPartyIndices)) + for i, idx := range signingPartyIndices { + participants[i] = Participant{ + PartyID: fmt.Sprintf("party-%d", idx), + PartyIndex: idx, + } + } + participantsJSON, _ := json.Marshal(participants) + + // Prepare encrypted shares + encryptedShares := make([]string, len(signingPartyIndices)) + for i, idx := range signingPartyIndices { + shareBytes, _ := json.Marshal(shares[idx]) + encrypted := encryptShare(shareBytes, password) + encryptedShares[i] = base64.StdEncoding.EncodeToString(encrypted) + } + + // Find tss-party executable + exePath := "./tss-party.exe" + if _, err := os.Stat(exePath); os.IsNotExist(err) { + exePath = "./tss-party" + if _, err := os.Stat(exePath); os.IsNotExist(err) { + return nil, fmt.Errorf("tss-party executable not found") + } + } + + t.Logf("Using executable: %s", exePath) + + // Start processes + processes := make([]*exec.Cmd, len(signingPartyIndices)) + stdinPipes := make([]io.WriteCloser, len(signingPartyIndices)) + stdoutPipes := make([]io.ReadCloser, len(signingPartyIndices)) + + for i, idx := range signingPartyIndices { + args := []string{ + "sign", + "-session-id", sessionID, + "-party-id", fmt.Sprintf("party-%d", idx), + "-party-index", fmt.Sprintf("%d", idx), + "-threshold-t", fmt.Sprintf("%d", thresholdT), + "-threshold-n", fmt.Sprintf("%d", thresholdN), + "-participants", string(participantsJSON), + "-message-hash", messageHash, + "-share-data", encryptedShares[i], + "-password", password, + } + + t.Logf("[Party %d] Starting with args: sign -session-id %s -party-id party-%d ...", idx, sessionID, idx) + + cmd := exec.Command(exePath, args...) + stdin, _ := cmd.StdinPipe() + stdout, _ := cmd.StdoutPipe() + cmd.Stderr = os.Stderr + + processes[i] = cmd + stdinPipes[i] = stdin + stdoutPipes[i] = stdout + + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start party %d: %w", idx, err) + } + } + + // Message routing between parties + var wg sync.WaitGroup + results := make([][]byte, len(signingPartyIndices)) + errors := make([]error, len(signingPartyIndices)) + + // Create a mutex for stdin writes + stdinMutexes := make([]*sync.Mutex, len(signingPartyIndices)) + for i := range stdinMutexes { + stdinMutexes[i] = &sync.Mutex{} + } + + for i := range signingPartyIndices { + wg.Add(1) + go func(partyIdx int) { + defer wg.Done() + scanner := bufio.NewScanner(stdoutPipes[partyIdx]) + scanner.Buffer(make([]byte, 1024*1024), 1024*1024) + + for scanner.Scan() { + line := scanner.Text() + var msg Message + if err := json.Unmarshal([]byte(line), &msg); err != nil { + t.Logf("[Party %d] Invalid JSON: %s", signingPartyIndices[partyIdx], line) + continue + } + + switch msg.Type { + case "progress": + t.Logf("[Party %d] Progress: round %d/%d", signingPartyIndices[partyIdx], msg.Round, msg.TotalRounds) + + case "outgoing": + // tss-party.exe outputs "outgoing" messages that need to be routed to other parties + t.Logf("[Party %d] Outgoing message (broadcast=%v, toParties=%v)", signingPartyIndices[partyIdx], msg.IsBroadcast, msg.ToParties) + // Route to other parties + for j := range signingPartyIndices { + if j != partyIdx { + // If not broadcast, check if this party is in the ToParties list + if !msg.IsBroadcast && len(msg.ToParties) > 0 { + targetPartyID := fmt.Sprintf("party-%d", signingPartyIndices[j]) + found := false + for _, to := range msg.ToParties { + if to == targetPartyID { + found = true + break + } + } + if !found { + continue + } + } + + // tss-party.exe expects "incoming" messages + msgToSend := Message{ + Type: "incoming", + IsBroadcast: msg.IsBroadcast, + Payload: msg.Payload, + FromPartyIndex: signingPartyIndices[partyIdx], + } + data, _ := json.Marshal(msgToSend) + + stdinMutexes[j].Lock() + stdinPipes[j].Write(append(data, '\n')) + stdinMutexes[j].Unlock() + } + } + + case "result": + t.Logf("[Party %d] Got result!", signingPartyIndices[partyIdx]) + signature, _ := base64.StdEncoding.DecodeString(msg.Payload) + results[partyIdx] = signature + + case "error": + t.Logf("[Party %d] Error: %s", signingPartyIndices[partyIdx], msg.Error) + errors[partyIdx] = fmt.Errorf("party error: %s", msg.Error) + } + } + + if err := scanner.Err(); err != nil { + t.Logf("[Party %d] Scanner error: %v", signingPartyIndices[partyIdx], err) + } + }(i) + } + + // Wait for processes to complete + for i, cmd := range processes { + if err := cmd.Wait(); err != nil { + t.Logf("[Party %d] Process exit error: %v", signingPartyIndices[i], err) + } + } + + wg.Wait() + + // Check for errors + for i, err := range errors { + if err != nil { + return nil, fmt.Errorf("party %d error: %w", signingPartyIndices[i], err) + } + } + + // Return first result + for _, result := range results { + if result != nil { + return result, nil + } + } + + return nil, fmt.Errorf("no signature received") +} diff --git a/backend/mpc-system/services/service-party-app/tss-party/main.go b/backend/mpc-system/services/service-party-app/tss-party/main.go index 9ef7f82c..75dd00cb 100644 --- a/backend/mpc-system/services/service-party-app/tss-party/main.go +++ b/backend/mpc-system/services/service-party-app/tss-party/main.go @@ -8,6 +8,7 @@ import ( "bufio" "context" "encoding/base64" + "encoding/hex" "encoding/json" "flag" "fmt" @@ -18,7 +19,9 @@ import ( "syscall" "time" + "github.com/bnb-chain/tss-lib/v2/common" "github.com/bnb-chain/tss-lib/v2/ecdsa/keygen" + "github.com/bnb-chain/tss-lib/v2/ecdsa/signing" "github.com/bnb-chain/tss-lib/v2/tss" ) @@ -118,9 +121,73 @@ func runKeygen() { } func runSign() { - // TODO: Implement signing - sendError("Signing not implemented yet") - os.Exit(1) + // Parse sign flags + fs := flag.NewFlagSet("sign", flag.ExitOnError) + sessionID := fs.String("session-id", "", "Session ID") + partyID := fs.String("party-id", "", "Party ID") + partyIndex := fs.Int("party-index", 0, "Party index (0-based)") + thresholdT := fs.Int("threshold-t", 0, "Threshold T") + thresholdN := fs.Int("threshold-n", 0, "Threshold N (total parties in keygen)") + participantsJSON := fs.String("participants", "[]", "Participants JSON array") + messageHash := fs.String("message-hash", "", "Message hash to sign (hex encoded)") + shareData := fs.String("share-data", "", "Encrypted share data (base64 encoded)") + password := fs.String("password", "", "Password to decrypt share") + + if err := fs.Parse(os.Args[2:]); err != nil { + sendError(fmt.Sprintf("Failed to parse flags: %v", err)) + os.Exit(1) + } + + // Validate required fields + if *sessionID == "" || *partyID == "" || *thresholdT == 0 || *thresholdN == 0 { + sendError("Missing required parameters") + os.Exit(1) + } + + if *messageHash == "" { + sendError("Missing message hash") + os.Exit(1) + } + + if *shareData == "" { + sendError("Missing share data") + os.Exit(1) + } + + // Parse participants + var participants []Participant + if err := json.Unmarshal([]byte(*participantsJSON), &participants); err != nil { + sendError(fmt.Sprintf("Failed to parse participants: %v", err)) + os.Exit(1) + } + + // Note: For signing, participant count equals threshold T (not N) + // because only T parties participate in signing + if len(participants) != *thresholdT { + sendError(fmt.Sprintf("Participant count mismatch: got %d, expected %d (threshold T)", len(participants), *thresholdT)) + os.Exit(1) + } + + // Run sign protocol + result, err := executeSign( + *sessionID, + *partyID, + *partyIndex, + *thresholdT, + *thresholdN, + participants, + *messageHash, + *shareData, + *password, + ) + + if err != nil { + sendError(fmt.Sprintf("Sign failed: %v", err)) + os.Exit(1) + } + + // Send result + sendSignResult(result.Signature, result.RecoveryID, *partyIndex) } type keygenResult struct { @@ -128,6 +195,11 @@ type keygenResult struct { EncryptedShare []byte } +type signResult struct { + Signature []byte + RecoveryID int +} + func executeKeygen( sessionID, partyID string, partyIndex, thresholdT, thresholdN int, @@ -404,3 +476,230 @@ func sendResult(publicKey, encryptedShare []byte, partyIndex int) { data, _ := json.Marshal(msg) fmt.Println(string(data)) } + +func sendSignResult(signature []byte, recoveryID int, partyIndex int) { + msg := Message{ + Type: "result", + Payload: base64.StdEncoding.EncodeToString(signature), + PartyIndex: partyIndex, + } + data, _ := json.Marshal(msg) + fmt.Println(string(data)) +} + +func decryptShare(encryptedData []byte, password string) ([]byte, error) { + // Match the encryption format: first 32 bytes are password hash, rest is data + if len(encryptedData) < 32 { + return nil, fmt.Errorf("encrypted data too short") + } + + // Verify password (simple check - matches encryptShare) + expectedHash := hashPassword(password) + actualHash := encryptedData[:32] + + // Simple comparison + match := true + for i := 0; i < 32; i++ { + if expectedHash[i] != actualHash[i] { + match = false + break + } + } + + if !match { + return nil, fmt.Errorf("incorrect password") + } + + return encryptedData[32:], nil +} + +func executeSign( + sessionID, partyID string, + partyIndex, thresholdT, thresholdN int, + participants []Participant, + messageHashHex string, + shareDataBase64 string, + password string, +) (*signResult, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + // Handle signals for graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigChan + cancel() + }() + + // Decode and decrypt share data + encryptedShare, err := base64.StdEncoding.DecodeString(shareDataBase64) + if err != nil { + return nil, fmt.Errorf("failed to decode share data: %w", err) + } + + shareBytes, err := decryptShare(encryptedShare, password) + if err != nil { + return nil, fmt.Errorf("failed to decrypt share: %w", err) + } + + // Parse keygen save data + var keygenData keygen.LocalPartySaveData + if err := json.Unmarshal(shareBytes, &keygenData); err != nil { + return nil, fmt.Errorf("failed to parse keygen data: %w", err) + } + + // Decode message hash + messageHash, err := hex.DecodeString(messageHashHex) + if err != nil { + return nil, fmt.Errorf("failed to decode message hash: %w", err) + } + + if len(messageHash) != 32 { + return nil, fmt.Errorf("message hash must be 32 bytes, got %d", len(messageHash)) + } + + msgBigInt := new(big.Int).SetBytes(messageHash) + + // Create TSS party IDs for signing participants + // IMPORTANT: For tss-lib signing, we must reconstruct the party IDs in the same way + // as during keygen. The signing subset (T parties) must use their original keys from keygen. + // + // The keygenData.Ks contains the public keys for all N parties from keygen. + // We need to create party IDs that match the original keygen party structure, + // but only include the T parties that are participating in this signing session. + + // Create party IDs only for the signing participants + tssPartyIDs := make([]*tss.PartyID, 0, len(participants)) + var selfTSSID *tss.PartyID + + for _, p := range participants { + // Use the keygen key at this party's index + // The party key in tss-lib uses the key's big.Int representation + partyKey := tss.NewPartyID( + p.PartyID, + fmt.Sprintf("party-%d", p.PartyIndex), + big.NewInt(int64(p.PartyIndex+1)), + ) + tssPartyIDs = append(tssPartyIDs, partyKey) + if p.PartyID == partyID { + selfTSSID = partyKey + } + } + + if selfTSSID == nil { + return nil, fmt.Errorf("self party not found in participants") + } + + // Sort party IDs (important for tss-lib) + sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) + + // Create peer context and parameters + // For signing with T parties from an N-party keygen: + // - The peer context contains only the T signing parties + // - threshold parameter should be T-1 (since we need T parties to sign, threshold = T-1) + peerCtx := tss.NewPeerContext(sortedPartyIDs) + params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, len(sortedPartyIDs), thresholdT-1) + + // Create channels + outCh := make(chan tss.Message, thresholdT*10) + endCh := make(chan *common.SignatureData, 1) + errCh := make(chan error, 1) + + // Create local party for signing + localParty := signing.NewLocalParty(msgBigInt, params, keygenData, outCh, endCh) + + // Build party index map for incoming messages + partyIndexMap := make(map[int]*tss.PartyID) + for _, p := range sortedPartyIDs { + for _, orig := range participants { + if orig.PartyID == p.Id { + partyIndexMap[orig.PartyIndex] = p + break + } + } + } + + // Start the local party + go func() { + if err := localParty.Start(); err != nil { + errCh <- err + } + }() + + // Handle outgoing messages + var outWg sync.WaitGroup + outWg.Add(1) + go func() { + defer outWg.Done() + for { + select { + case <-ctx.Done(): + return + case msg, ok := <-outCh: + if !ok { + return + } + handleOutgoingMessage(msg) + } + } + }() + + // Handle incoming messages from stdin + var inWg sync.WaitGroup + inWg.Add(1) + go func() { + defer inWg.Done() + scanner := bufio.NewScanner(os.Stdin) + // 增加 buffer 大小到 1MB + scanner.Buffer(make([]byte, 1024*1024), 1024*1024) + for scanner.Scan() { + select { + case <-ctx.Done(): + return + default: + } + + var msg Message + if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil { + continue + } + + if msg.Type == "incoming" { + handleIncomingMessage(msg, localParty, partyIndexMap, errCh) + } + } + }() + + // Track progress - GG20 signing has 9 rounds + totalRounds := 9 + + // Wait for completion + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-errCh: + return nil, err + case sigData := <-endCh: + // Signing completed successfully + sendProgress(totalRounds, totalRounds) + + // Construct signature in DER format or raw R||S format + // sigData contains R, S, and recovery ID + rBytes := sigData.R + sBytes := sigData.S + + // Create raw signature: R (32 bytes) || S (32 bytes) + signature := make([]byte, 64) + copy(signature[32-len(rBytes):32], rBytes) + copy(signature[64-len(sBytes):64], sBytes) + + // Recovery ID for Ethereum-style signatures + recoveryID := int(sigData.SignatureRecovery[0]) + + return &signResult{ + Signature: signature, + RecoveryID: recoveryID, + }, nil + } +}