563 lines
16 KiB
TypeScript
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;
|
|
}
|
|
}
|