import { spawn, ChildProcess } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { EventEmitter } from 'events'; import { GrpcClient } from './grpc-client'; import { DatabaseManager } from './database'; /** * TSS 协议处理结果 */ export interface KeygenResult { success: boolean; publicKey: Buffer; encryptedShare: Buffer; partyIndex: number; error?: string; } export interface SignResult { success: boolean; signature: Buffer; error?: string; } /** * TSS 消息处理器接口 */ interface TSSMessage { type: 'outgoing' | 'result' | 'error' | 'progress'; isBroadcast?: boolean; toParties?: string[]; payload?: string; // base64 encoded publicKey?: string; // base64 encoded encryptedShare?: string; // base64 encoded partyIndex?: number; round?: number; totalRounds?: number; error?: string; } /** * 会话参与者信息 */ interface ParticipantInfo { partyId: string; partyIndex: number; } /** * TSS 协议处理器 * * 使用子进程方式运行 Go 编写的 TSS 协议实现 * 这种方式比 WASM 更可靠,特别是对于复杂的密码学操作 */ export class TSSHandler extends EventEmitter { private tssProcess: ChildProcess | null = null; private grpcClient: GrpcClient; private database: DatabaseManager | null = null; private sessionId: string | null = null; private partyId: string | null = null; private partyIndex: number = -1; private participants: ParticipantInfo[] = []; private partyIndexMap: Map = new Map(); private isRunning = false; // 消息缓冲:在 TSS 进程启动前缓冲收到的消息 private messageBuffer: Array<{ messageId: string; fromParty: string; isBroadcast: boolean; payload: Buffer; }> = []; private isProcessReady = false; // 是否已预订阅消息(用于在 keygen 启动前就开始缓冲消息) private isPrepared = false; constructor(grpcClient: GrpcClient, database?: DatabaseManager) { super(); this.grpcClient = grpcClient; this.database = database || null; } /** * 设置数据库管理器(用于消息去重) */ setDatabase(database: DatabaseManager): void { this.database = database; } /** * 获取 TSS 二进制文件路径 */ private getTSSBinaryPath(): string { const platform = process.platform; const arch = process.arch; let binaryName = 'tss-party'; if (platform === 'win32') { binaryName += '.exe'; } // 开发环境: 在项目目录下 const devPath = path.join(__dirname, '../../bin', `${platform}-${arch}`, binaryName); if (fs.existsSync(devPath)) { return devPath; } // 生产环境: 在 app 资源目录下 const prodPath = path.join(process.resourcesPath, 'bin', binaryName); if (fs.existsSync(prodPath)) { return prodPath; } // 回退: 期望在 PATH 中 return binaryName; } /** * 预订阅消息流 - 在 joinSession 后立即调用 * 这确保在其他方开始发送消息时,我们已经准备好接收和缓冲 * * @param sessionId 会话 ID * @param partyId 自己的 party ID */ prepareForKeygen(sessionId: string, partyId: string): void { // 检查 gRPC 连接状态 if (!this.grpcClient.isConnected()) { console.error('[TSS] Cannot prepare for keygen: gRPC client not connected'); throw new Error('gRPC client not connected'); } // 如果已经为同一个 session 准备过,跳过 if (this.isPrepared && this.sessionId === sessionId) { console.log('[TSS] Already prepared for same session, skip'); return; } // 如果为不同的 session 准备过,先取消旧的订阅 if (this.isPrepared && this.sessionId !== sessionId) { console.log(`[TSS] 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] Preparing for keygen: session=${sessionId.substring(0, 8)}..., party=${partyId.substring(0, 8)}...`); this.sessionId = sessionId; this.partyId = partyId; this.isPrepared = true; this.messageBuffer = []; // 立即订阅消息流,开始缓冲消息 // 使用 bound 方法确保 this 上下文正确 this.grpcClient.on('mpcMessage', this.handleIncomingMessage.bind(this)); this.grpcClient.subscribeMessages(sessionId, partyId); console.log('[TSS] Message subscription started, buffering enabled'); } /** * 取消预订阅 */ cancelPrepare(): void { if (!this.isPrepared) { return; } console.log('[TSS] Canceling prepare'); this.isPrepared = false; this.messageBuffer = []; this.grpcClient.removeAllListeners('mpcMessage'); this.grpcClient.unsubscribeMessages(); this.sessionId = null; this.partyId = null; } /** * 参与 Keygen 协议 */ async participateKeygen( sessionId: string, partyId: string, partyIndex: number, participants: ParticipantInfo[], threshold: { t: number; n: number }, encryptionPassword: 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] Starting keygen: 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] Binary path: ${binaryPath}`); console.log(`[TSS] Binary exists: ${fs.existsSync(binaryPath)}`); // 构建参与者列表 JSON const participantsJson = JSON.stringify(participants); console.log(`[TSS] Participants: ${participantsJson}`); console.log(`[TSS] partyIndex=${partyIndex}, threshold=${threshold.t}-of-${threshold.n}`); // 启动 TSS 子进程 const args = [ 'keygen', '--session-id', sessionId, '--party-id', partyId, '--party-index', partyIndex.toString(), '--threshold-t', threshold.t.toString(), '--threshold-n', threshold.n.toString(), '--participants', participantsJson, '--password', encryptionPassword, ]; console.log(`[TSS] Spawning: ${binaryPath} ${args.join(' ')}`); this.tssProcess = spawn(binaryPath, args); let resultData = ''; let stderrData = ''; // 如果没有预订阅,现在订阅消息 // 如果已经预订阅,消息监听器已经注册,不需要重复注册 if (!wasPrepared) { console.log('[TSS] Subscribing to messages (not prepared before)'); this.grpcClient.on('mpcMessage', this.handleIncomingMessage.bind(this)); this.grpcClient.subscribeMessages(sessionId, partyId); } else { console.log(`[TSS] 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] 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]', line); } } }); // 处理标准错误 this.tssProcess.stderr?.on('data', (data: Buffer) => { const errorText = data.toString(); stderrData += errorText; console.error('[TSS 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.sessionId = null; this.partyId = null; // 清理消息监听器和 gRPC 流订阅,防止下次 keygen 时出错 this.grpcClient.removeAllListeners('mpcMessage'); this.grpcClient.unsubscribeMessages(); if (code === 0 && resultData) { try { const result: TSSMessage = JSON.parse(resultData); if (result.publicKey && result.encryptedShare) { // 成功完成后清理该会话的已处理消息记录 if (this.database && completedSessionId) { this.database.clearProcessedMessages(completedSessionId); } resolve({ success: true, publicKey: Buffer.from(result.publicKey, 'base64'), encryptedShare: Buffer.from(result.encryptedShare, 'base64'), partyIndex: result.partyIndex || partyIndex, }); } else { reject(new Error(result.error || 'Keygen failed: no result data')); } } catch (e) { reject(new Error(`Failed to parse keygen result: ${e}`)); } } else { const errorMsg = stderrData.trim() || `Keygen process exited with code ${code}`; console.error(`[TSS] 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.sessionId = null; this.partyId = null; // 清理消息监听器和 gRPC 流订阅 this.grpcClient.removeAllListeners('mpcMessage'); this.grpcClient.unsubscribeMessages(); reject(err); }); } catch (err) { this.isRunning = false; this.isPrepared = false; this.sessionId = null; this.partyId = null; reject(err); } }); } /** * 处理 TSS 进程发出的消息 */ private async handleTSSMessage(message: TSSMessage): Promise { switch (message.type) { case 'outgoing': // TSS 进程要发送消息给其他参与者 if (message.payload && this.sessionId && this.partyId) { const payload = Buffer.from(message.payload, 'base64'); const toParties = message.isBroadcast ? [] : (message.toParties || []); try { await this.grpcClient.routeMessage( this.sessionId, this.partyId, toParties, 0, // round number handled by TSS lib payload ); } catch (err) { console.error('Failed to route TSS message:', err); } } break; case 'progress': // 进度更新 this.emit('progress', { round: message.round, totalRounds: message.totalRounds, }); break; case 'error': this.emit('error', new Error(message.error)); break; case 'result': // 结果会在 close 事件中处理 break; } } /** * 处理从 gRPC 接收的 MPC 消息(带去重) * * 消息处理状态: * 1. isPrepared=true, isRunning=false: 预订阅阶段,缓冲消息 * 2. isPrepared=true, isRunning=true, isProcessReady=false: 进程启动中,缓冲消息 * 3. isPrepared=true, isRunning=true, isProcessReady=true: 进程就绪,直接发送 * * Note: gRPC message uses snake_case field names due to keepCase: true in proto-loader */ private handleIncomingMessage(rawMessage: { message_id: string; from_party: string; is_broadcast: boolean; payload: Buffer; }): void { // 转换为内部使用的 camelCase 格式 const message = { messageId: rawMessage.message_id, fromParty: rawMessage.from_party, isBroadcast: rawMessage.is_broadcast, payload: rawMessage.payload, }; // 消息去重检查 if (this.database && message.messageId) { if (this.database.isMessageProcessed(message.messageId)) { console.log(`[TSS] Skipping duplicate message: ${message.messageId.substring(0, 8)}...`); return; } } // 如果进程就绪,直接发送 if (this.isProcessReady && this.tssProcess?.stdin) { this.sendMessageToProcess(message); return; } // 如果已预订阅或正在运行,缓冲消息 // 这确保在任何阶段收到的消息都不会丢失 if (this.isPrepared || this.isRunning) { console.log(`[TSS] Buffering message from ${message.fromParty.substring(0, 8)}... (prepared=${this.isPrepared}, running=${this.isRunning}, ready=${this.isProcessReady})`); this.messageBuffer.push(message); return; } // 既没预订阅也没运行,忽略消息 console.log(`[TSS] Ignoring message from ${message.fromParty.substring(0, 8)}... (not prepared)`); } /** * 发送消息给 TSS 进程(并标记为已处理) */ private sendMessageToProcess(message: { messageId: string; fromParty: string; isBroadcast: boolean; payload: Buffer; }): void { if (!this.tssProcess?.stdin) { return; } const fromIndex = this.partyIndexMap.get(message.fromParty); if (fromIndex === undefined) { console.warn('Received message from unknown party:', message.fromParty); return; } // 发送消息给 TSS 进程 const inputMessage = JSON.stringify({ type: 'incoming', fromPartyIndex: fromIndex, isBroadcast: message.isBroadcast, payload: message.payload.toString('base64'), }); this.tssProcess.stdin.write(inputMessage + '\n'); // 标记消息为已处理(防止重连后重复处理) if (this.database && message.messageId && this.sessionId) { this.database.markMessageProcessed(message.messageId, this.sessionId); } } /** * 发送缓冲的消息 */ private flushMessageBuffer(): void { if (this.messageBuffer.length === 0) { return; } console.log(`[TSS] Flushing ${this.messageBuffer.length} buffered messages`); for (const msg of this.messageBuffer) { this.sendMessageToProcess(msg); } this.messageBuffer = []; } // =========================================================================== // Co-Sign 相关方法 - 与 Keygen 完全隔离的签名功能 // =========================================================================== /** * 预订阅签名消息流 - 在 joinSession 后立即调用 * 与 prepareForKeygen 类似,确保在其他方开始发送消息时已准备好接收和缓冲 * * @param sessionId 签名会话 ID * @param partyId 自己的 party ID */ prepareForSign(sessionId: string, partyId: string): void { // 检查 gRPC 连接状态 if (!this.grpcClient.isConnected()) { console.error('[TSS-SIGN] Cannot prepare for sign: gRPC client not connected'); throw new Error('gRPC client not connected'); } // 如果已经为同一个 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.sessionId = null; this.partyId = null; // 清理消息监听器和 gRPC 流订阅 this.grpcClient.removeAllListeners('mpcMessage'); this.grpcClient.unsubscribeMessages(); 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.sessionId = null; this.partyId = null; // 清理消息监听器和 gRPC 流订阅 this.grpcClient.removeAllListeners('mpcMessage'); this.grpcClient.unsubscribeMessages(); reject(err); }); } catch (err) { this.isRunning = false; this.isPrepared = false; this.sessionId = null; this.partyId = null; reject(err); } }); } /** * 取消正在进行的协议 */ cancel(): void { if (this.tssProcess) { this.tssProcess.kill('SIGTERM'); this.tssProcess = null; } this.isRunning = false; this.isProcessReady = false; this.isPrepared = false; this.messageBuffer = []; this.sessionId = null; this.partyId = null; // 清理消息监听器和 gRPC 流订阅 this.grpcClient.removeAllListeners('mpcMessage'); this.grpcClient.unsubscribeMessages(); } /** * 检查是否正在运行 */ getIsRunning(): boolean { return this.isRunning; } /** * 检查是否已预订阅 */ getIsPrepared(): boolean { return this.isPrepared; } } /** * 模拟 TSS Handler * * 用于开发和测试,不需要实际的 TSS 二进制文件 * 模拟 keygen 过程并生成假的密钥数据 */ export class MockTSSHandler extends EventEmitter { private grpcClient: GrpcClient; private isRunning = false; constructor(grpcClient: GrpcClient) { super(); this.grpcClient = grpcClient; } async participateKeygen( sessionId: string, partyId: string, partyIndex: number, participants: ParticipantInfo[], threshold: { t: number; n: number }, encryptionPassword: string ): Promise { if (this.isRunning) { throw new Error('TSS protocol already running'); } this.isRunning = true; // 模拟 keygen 过程 const totalRounds = 4; for (let round = 1; round <= totalRounds; round++) { // 每轮等待 1-2 秒 await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000)); this.emit('progress', { round, totalRounds, }); } this.isRunning = false; // 生成模拟的密钥数据 const mockPublicKey = Buffer.alloc(33); mockPublicKey[0] = 0x02; // compressed public key prefix for (let i = 1; i < 33; i++) { mockPublicKey[i] = Math.floor(Math.random() * 256); } const mockShareData = Buffer.from(JSON.stringify({ sessionId, partyId, partyIndex, threshold, participants: participants.map(p => ({ partyId: p.partyId, partyIndex: p.partyIndex })), createdAt: new Date().toISOString(), // 实际的 share 数据会更复杂 shareSecret: Buffer.alloc(32).fill(partyIndex).toString('hex'), })); // 简单的 "加密" (实际应该使用 AES-256-GCM) const encryptedShare = Buffer.concat([ Buffer.from('MOCK_ENCRYPTED:'), mockShareData, ]); return { success: true, publicKey: mockPublicKey, encryptedShare, partyIndex, }; } cancel(): void { this.isRunning = false; } getIsRunning(): boolean { return this.isRunning; } }