rwadurian/backend/mpc-system/services/service-party-app/electron/modules/tss-handler.ts

563 lines
16 KiB
TypeScript

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<string, number> = 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 {
if (this.isPrepared) {
console.log('[TSS] Already prepared for keygen, skip');
return;
}
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<KeygenResult> {
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();
// 构建参与者列表 JSON
const participantsJson = JSON.stringify(participants);
// 启动 TSS 子进程
this.tssProcess = spawn(binaryPath, [
'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,
]);
let resultData = '';
// 如果没有预订阅,现在订阅消息
// 如果已经预订阅,消息监听器已经注册,不需要重复注册
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) => {
console.error('[TSS Error]', data.toString());
});
// 处理进程退出
this.tssProcess.on('close', (code) => {
const completedSessionId = this.sessionId;
this.isRunning = false;
this.isProcessReady = false;
this.isPrepared = false;
this.messageBuffer = [];
this.tssProcess = null;
// 清理消息监听器,防止下次 keygen 时重复注册
this.grpcClient.removeAllListeners('mpcMessage');
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 {
reject(new Error(`Keygen process exited with code ${code}`));
}
});
// 处理进程错误
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);
}
});
}
/**
* 处理 TSS 进程发出的消息
*/
private async handleTSSMessage(message: TSSMessage): Promise<void> {
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: 进程就绪,直接发送
*/
private handleIncomingMessage(message: {
messageId: string;
fromParty: string;
isBroadcast: boolean;
payload: Buffer;
}): void {
// 消息去重检查
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 = [];
}
/**
* 取消正在进行的协议
*/
cancel(): void {
if (this.tssProcess) {
this.tssProcess.kill('SIGTERM');
this.tssProcess = null;
}
this.isRunning = false;
this.isProcessReady = false;
this.isPrepared = false;
this.messageBuffer = [];
this.grpcClient.removeAllListeners('mpcMessage');
}
/**
* 检查是否正在运行
*/
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<KeygenResult> {
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;
}
}