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 a1ed3514..47eeceb6 100644 --- a/backend/mpc-system/services/service-party-app/electron/main.ts +++ b/backend/mpc-system/services/service-party-app/electron/main.ts @@ -24,7 +24,7 @@ const USE_MOCK_TSS = process.env.USE_MOCK_TSS === 'true'; let debugLogEnabled = false; type LogLevel = 'info' | 'warn' | 'error' | 'debug'; -type LogSource = 'main' | 'grpc' | 'tss' | 'account' | 'renderer'; +type LogSource = 'main' | 'grpc' | 'tss' | 'sign' | 'account' | 'renderer'; function sendDebugLog(level: LogLevel, source: LogSource, message: string) { if (debugLogEnabled && mainWindow) { @@ -1080,7 +1080,7 @@ function setupIpcHandlers() { } }); - // gRPC - 加入签名会话 + // gRPC - 加入签名会话并执行签名协议 ipcMain.handle('grpc:joinSigningSession', async (_event, params) => { try { // 从本地 SQLite 获取 share 数据 @@ -1089,19 +1089,121 @@ function setupIpcHandlers() { return { success: false, error: 'Share not found or incorrect password' }; } + // 验证 keygen_session_id 匹配 + if (params.keygenSessionId && share.session_id !== params.keygenSessionId) { + return { success: false, error: 'Share does not match the keygen session' }; + } + // 加入签名会话 (通过 gRPC) - // TODO: 实际加入会话逻辑需要使用 gRPC client - // 这里先返回成功,表示验证通过 + const joinResult = await grpcClient?.joinSession( + params.sessionId, + share.party_id, + params.joinToken + ); + + if (!joinResult?.success) { + return { success: false, error: 'Failed to join signing session' }; + } + return { success: true, partyId: share.party_id, partyIndex: share.party_index, + sessionInfo: joinResult.session_info, + otherParties: joinResult.other_parties, }; } catch (error) { return { success: false, error: (error as Error).message }; } }); + // 执行签名协议 + ipcMain.handle('grpc:executeSign', async (_event, params: { + sessionId: string; + shareId: string; + password: string; + messageHash: string; // hex encoded + participants: Array<{ partyId: string; partyIndex: number }>; + threshold: { t: number; n: number }; + }) => { + try { + debugLog.info('sign', `Starting sign protocol for session ${params.sessionId}`); + + // 从本地 SQLite 获取 share 数据 + const share = database?.getShare(params.shareId, params.password); + if (!share) { + return { success: false, error: 'Share not found or incorrect password' }; + } + + // 验证 partyId 是否在 participants 中 + const myParticipant = params.participants.find(p => p.partyId === share.party_id); + if (!myParticipant) { + return { success: false, error: 'Party not found in participants list' }; + } + + // 解析 raw_share (这是加密后的 TSS save data) + // raw_share 格式: base64(32-byte password hash + JSON save data) + const rawShareBytes = Buffer.from(share.raw_share, 'base64'); + if (rawShareBytes.length <= 32) { + return { success: false, error: 'Invalid share data' }; + } + // 跳过前 32 字节的 password hash,取后面的 JSON 数据 + const shareData = rawShareBytes.subarray(32); + + // 转换 messageHash 从 hex 到 Buffer + const messageHashBuffer = Buffer.from(params.messageHash, 'hex'); + if (messageHashBuffer.length !== 32) { + return { success: false, error: 'Invalid message hash length (must be 32 bytes)' }; + } + + debugLog.info('sign', `Executing TSS sign protocol: partyId=${share.party_id.substring(0, 8)}..., partyIndex=${myParticipant.partyIndex}`); + + // 执行签名协议 (使用类型断言,因为 MockTSSHandler 没有实现 participateSign) + if (!tssHandler || !('participateSign' in tssHandler)) { + return { success: false, error: 'TSS handler does not support signing' }; + } + const result = await (tssHandler as TSSHandler).participateSign( + params.sessionId, + share.party_id, + myParticipant.partyIndex, + params.participants, + params.threshold, + messageHashBuffer, + shareData + ); + + if (!result?.success) { + return { success: false, error: result?.error || 'Signing failed' }; + } + + debugLog.info('sign', `Sign completed successfully`); + + // 保存签名历史 + const history = database?.createSigningHistory({ + shareId: params.shareId, + sessionId: params.sessionId, + messageHash: params.messageHash, + }); + if (history) { + database?.updateSigningHistory(history.id, { + status: 'completed', + signature: result.signature.toString('hex'), + }); + } + + return { + success: true, + signature: result.signature.toString('hex'), + r: result.r, + s: result.s, + recoveryId: result.recoveryId, + }; + } catch (error) { + debugLog.error('sign', `Sign failed: ${(error as Error).message}`); + return { success: false, error: (error as Error).message }; + } + }); + // =========================================================================== // Account 服务相关 (HTTP API) // =========================================================================== @@ -1222,8 +1324,9 @@ function setupIpcHandlers() { participants: share.participants || [], }, password); - // 自动派生 Kava 地址 + // 自动派生地址 (Kava Cosmos + Kava EVM) if (saved && share.publicKey) { + // 派生 Kava Cosmos 地址 try { const kavaAddress = addressDerivationService.deriveAddress(share.publicKey, 'kava'); database?.saveDerivedAddress({ @@ -1236,6 +1339,20 @@ function setupIpcHandlers() { } catch (err) { console.error('Failed to derive Kava address:', err); } + + // 派生 Kava EVM 地址 (0x 格式,与 Ethereum 地址相同) + try { + const evmAddress = addressDerivationService.deriveAddress(share.publicKey, 'ethereum'); + database?.saveDerivedAddress({ + shareId: saved.id, + chain: 'kava_evm', + derivationPath: evmAddress.derivationPath, + address: evmAddress.address, + publicKeyHex: share.publicKey, + }); + } catch (err) { + console.error('Failed to derive Kava EVM address:', err); + } } return { success: true, data: saved }; @@ -1606,6 +1723,18 @@ function setupIpcHandlers() { return result.canceled ? null : result.filePath; }); + // 写入文件(用于导出备份) + ipcMain.handle('file:write', async (_event, { filePath, data }) => { + try { + // data 可能是 Buffer 或 Uint8Array,需要转换 + const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data); + fs.writeFileSync(filePath, buffer); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + // =========================================================================== // 调试相关 // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/electron/modules/address-derivation.ts b/backend/mpc-system/services/service-party-app/electron/modules/address-derivation.ts index 541af1bb..831c06e0 100644 --- a/backend/mpc-system/services/service-party-app/electron/modules/address-derivation.ts +++ b/backend/mpc-system/services/service-party-app/electron/modules/address-derivation.ts @@ -21,6 +21,13 @@ export const CHAIN_CONFIGS: Record = { curve: 'secp256k1', derivationPath: "m/44'/459'/0'/0/0", }, + kava_evm: { + name: 'Kava EVM', + prefix: '0x', + coinType: 60, + curve: 'secp256k1', + derivationPath: "m/44'/60'/0'/0/0", + }, cosmos: { name: 'Cosmos Hub', prefix: 'cosmos', @@ -263,7 +270,8 @@ export class AddressDerivationService { let address: string; - if (chain === 'ethereum') { + if (chain === 'ethereum' || chain === 'kava_evm') { + // EVM 兼容地址 (0x 格式) address = deriveEthereumAddress(publicKeyHex); } else if (config.curve === 'ed25519') { address = deriveEd25519Address(publicKeyHex, config.prefix); 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..30b474e8 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 @@ -19,6 +19,9 @@ export interface KeygenResult { export interface SignResult { success: boolean; signature: Buffer; + r: string; // hex encoded + s: string; // hex encoded + recoveryId: number; error?: string; } @@ -26,7 +29,7 @@ export interface SignResult { * TSS 消息处理器接口 */ interface TSSMessage { - type: 'outgoing' | 'result' | 'error' | 'progress'; + type: 'outgoing' | 'result' | 'sign_result' | 'error' | 'progress'; isBroadcast?: boolean; toParties?: string[]; payload?: string; // base64 encoded @@ -36,6 +39,11 @@ interface TSSMessage { round?: number; totalRounds?: number; error?: string; + // Signing result fields + signature?: string; // base64 encoded + r?: string; // hex encoded + s?: string; // hex encoded + recoveryId?: number; } /** @@ -477,6 +485,172 @@ export class TSSHandler extends EventEmitter { this.messageBuffer = []; } + /** + * 参与 Sign 协议 + * + * @param sessionId 签名会话 ID + * @param partyId 自己的 party ID + * @param partyIndex 自己的 party index (来自 keygen) + * @param participants 当前签名的参与者列表 (可能是 keygen 参与者的子集) + * @param threshold keygen 时的阈值参数 + * @param messageHash 要签名的消息哈希 (32 bytes) + * @param shareData 解密后的 keygen share 数据 (JSON bytes) + */ + async participateSign( + sessionId: string, + partyId: string, + partyIndex: number, + participants: ParticipantInfo[], + threshold: { t: number; n: number }, + messageHash: Buffer, + shareData: Buffer + ): Promise { + if (this.isRunning) { + throw new Error('TSS protocol already running'); + } + + console.log(`[TSS] Starting sign: session=${sessionId.substring(0, 8)}..., party=${partyId.substring(0, 8)}...`); + console.log(`[TSS] Sign participants: ${participants.length}, threshold=${threshold.t}-of-${threshold.n}`); + + this.sessionId = sessionId; + this.partyId = partyId; + this.partyIndex = partyIndex; + this.participants = participants; + this.isRunning = true; + this.isProcessReady = false; + this.isPrepared = true; + this.messageBuffer = []; + + // 构建 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}`); + + // 构建参与者列表 JSON + const participantsJson = JSON.stringify(participants); + + // 启动 TSS 子进程 + 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.toString('base64'), + '--share-data', shareData.toString('base64'), + ]; + console.log(`[TSS] Spawning sign process`); + + this.tssProcess = spawn(binaryPath, args); + + let resultData = ''; + let stderrData = ''; + + // 订阅消息 + console.log('[TSS] Subscribing to messages for signing'); + this.grpcClient.on('mpcMessage', this.handleIncomingMessage.bind(this)); + this.grpcClient.subscribeMessages(sessionId, partyId); + + // 处理标准输出 (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 === 'sign_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.grpcClient.removeAllListeners('mpcMessage'); + + if (code === 0 && resultData) { + try { + const result: TSSMessage = JSON.parse(resultData); + if (result.signature && result.r && result.s !== undefined) { + // 成功完成后清理该会话的已处理消息记录 + if (this.database && completedSessionId) { + this.database.clearProcessedMessages(completedSessionId); + } + resolve({ + success: true, + signature: Buffer.from(result.signature, 'base64'), + r: result.r, + s: result.s, + recoveryId: result.recoveryId || 0, + }); + } else { + reject(new Error(result.error || 'Signing failed: no result data')); + } + } catch (e) { + reject(new Error(`Failed to parse signing result: ${e}`)); + } + } else { + const errorMsg = stderrData.trim() || `Signing 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 f0cb6ac8..16818a59 100644 --- a/backend/mpc-system/services/service-party-app/electron/preload.ts +++ b/backend/mpc-system/services/service-party-app/electron/preload.ts @@ -71,8 +71,19 @@ contextBridge.exposeInMainWorld('electronAPI', { sessionId: string; shareId: string; password: string; + keygenSessionId?: string; + joinToken?: string; }) => ipcRenderer.invoke('grpc:joinSigningSession', params), + executeSign: (params: { + sessionId: string; + shareId: string; + password: string; + messageHash: string; // hex encoded + participants: Array<{ partyId: string; partyIndex: number }>; + threshold: { t: number; n: number }; + }) => ipcRenderer.invoke('grpc:executeSign', params), + subscribeSigningProgress: (sessionId: string, callback: (event: unknown) => void) => { const channel = `signing:progress:${sessionId}`; const listener = (_event: unknown, data: unknown) => callback(data); @@ -251,6 +262,14 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.invoke('dialog:saveFile', { defaultPath, filters }), }, + // =========================================================================== + // 文件操作相关 + // =========================================================================== + file: { + write: (filePath: string, data: Uint8Array) => + ipcRenderer.invoke('file:write', { filePath, data }), + }, + // =========================================================================== // 调试相关 // =========================================================================== diff --git a/backend/mpc-system/services/service-party-app/src/pages/Home.tsx b/backend/mpc-system/services/service-party-app/src/pages/Home.tsx index 504964d5..d660be06 100644 --- a/backend/mpc-system/services/service-party-app/src/pages/Home.tsx +++ b/backend/mpc-system/services/service-party-app/src/pages/Home.tsx @@ -11,9 +11,7 @@ interface ShareItem { threshold: { t: number; n: number }; createdAt: string; lastUsedAt?: string; - metadata: { - participants: Array<{ partyId: string; name: string }>; - }; + participants?: Array<{ partyId: string; name: string }>; } interface ShareWithAddress extends ShareItem { @@ -89,15 +87,14 @@ export default function Home() { const result = await window.electronAPI.storage.exportShare(id, password); if (result.success && result.data) { - // 通过 IPC 写入文件 - const blob = new Blob([result.data], { type: 'application/octet-stream' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `share-backup-${id.slice(0, 8)}.dat`; - a.click(); - URL.revokeObjectURL(url); - alert('备份文件导出成功!'); + // 使用 file:write IPC 写入文件到用户选择的路径 + const dataArray = result.data instanceof ArrayBuffer ? new Uint8Array(result.data) : result.data; + const writeResult = await window.electronAPI.file.write(savePath, dataArray); + if (writeResult.success) { + alert('备份文件导出成功!'); + } else { + alert('写入文件失败: ' + (writeResult.error || '未知错误')); + } } else { alert('导出失败: ' + (result.error || '未知错误')); } @@ -221,7 +218,7 @@ export default function Home() {
参与方 - {(share.metadata?.participants || []).length} 人 + {(share.participants || []).length || share.threshold.n} 人
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 76ee1496..35ec0fbd 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 @@ -191,12 +191,37 @@ interface JoinSigningSessionParams { sessionId: string; shareId: string; password: string; + keygenSessionId?: string; + joinToken?: string; } interface JoinSigningSessionResult { success: boolean; partyId?: string; partyIndex?: number; + sessionInfo?: { + threshold_t?: number; + threshold_n?: number; + }; + otherParties?: Array<{ party_id: string; party_index: number }>; + error?: string; +} + +interface ExecuteSignParams { + sessionId: string; + shareId: string; + password: string; + messageHash: string; // hex encoded + participants: Array<{ partyId: string; partyIndex: number }>; + threshold: { t: number; n: number }; +} + +interface ExecuteSignResult { + success: boolean; + signature?: string; // hex encoded + r?: string; // hex encoded + s?: string; // hex encoded + recoveryId?: number; error?: string; } @@ -447,6 +472,7 @@ interface ElectronAPI { // 签名相关 validateSigningSession: (code: string) => Promise; joinSigningSession: (params: JoinSigningSessionParams) => Promise; + executeSign: (params: ExecuteSignParams) => Promise; subscribeSigningProgress: (sessionId: string, callback: (event: SigningProgressEvent) => void) => () => void; }; @@ -513,6 +539,11 @@ interface ElectronAPI { saveFile: (defaultPath?: string, filters?: { name: string; extensions: string[] }[]) => Promise; }; + // 文件操作相关 + file: { + write: (filePath: string, data: Uint8Array) => Promise<{ success: boolean; error?: string }>; + }; + // 调试相关 debug?: { subscribeLogs: (callback: (event: unknown, data: { level: string; source: string; message: string }) => void) => void; 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..388c36f9 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 @@ -2,11 +2,18 @@ module github.com/rwadurian/mpc-system/services/service-party-app/tss-party go 1.21 -require github.com/bnb-chain/tss-lib/v2 v2.0.2 +require ( + github.com/bnb-chain/tss-lib/v2 v2.0.2 + github.com/btcsuite/btcd/btcec/v2 v2.3.2 + github.com/stretchr/testify v1.8.4 +) require ( github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect + github.com/btcsuite/btcd v0.23.4 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect + github.com/btcsuite/btcutil v1.0.2 // indirect + github.com/davecgh/go-spew v1.1.1 // 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 @@ -18,10 +25,13 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // 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 + gopkg.in/yaml.v3 v3.0.1 // indirect ) // Replace to fix tss-lib dependency issue with ed25519 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..ba5244eb 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= @@ -74,9 +77,12 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -144,6 +150,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= @@ -234,6 +241,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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 e613f701..46908272 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 @@ -18,7 +18,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" ) @@ -35,6 +37,11 @@ type Message struct { TotalRounds int `json:"totalRounds,omitempty"` FromPartyIndex int `json:"fromPartyIndex,omitempty"` Error string `json:"error,omitempty"` + // Signing result fields + Signature string `json:"signature,omitempty"` // base64 encoded (R || S, 64 bytes) + R string `json:"r,omitempty"` // hex encoded + S string `json:"s,omitempty"` // hex encoded + RecoveryID int `json:"recoveryId,omitempty"` // for ecrecover } // Participant info @@ -118,9 +125,83 @@ 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, "Original Threshold N from keygen") + participantsJSON := fs.String("participants", "[]", "Participants JSON array (current signers)") + messageHashB64 := fs.String("message-hash", "", "Message hash to sign (base64 encoded)") + shareDataB64 := fs.String("share-data", "", "Decrypted share data from keygen (base64 encoded)") + + 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 *messageHashB64 == "" { + sendError("Missing message hash") + os.Exit(1) + } + + if *shareDataB64 == "" { + sendError("Missing share data") + os.Exit(1) + } + + // Parse participants (current signers, may be subset of original keygen 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) + } + + if len(participants) < *thresholdT { + sendError(fmt.Sprintf("Not enough signers: got %d, need at least %d", len(participants), *thresholdT)) + os.Exit(1) + } + + // Decode message hash + messageHash, err := base64.StdEncoding.DecodeString(*messageHashB64) + if err != nil { + sendError(fmt.Sprintf("Failed to decode message hash: %v", err)) + os.Exit(1) + } + + // Decode share data + shareData, err := base64.StdEncoding.DecodeString(*shareDataB64) + if err != nil { + sendError(fmt.Sprintf("Failed to decode share data: %v", err)) + os.Exit(1) + } + + // Run sign protocol + result, err := executeSign( + *sessionID, + *partyID, + *partyIndex, + *thresholdT, + *thresholdN, + participants, + messageHash, + shareData, + ) + + if err != nil { + sendError(fmt.Sprintf("Signing failed: %v", err)) + os.Exit(1) + } + + // Send result + sendSignResult(result.Signature, result.R, result.S, result.RecoveryID) } type keygenResult struct { @@ -405,3 +486,237 @@ func sendResult(publicKey, encryptedShare []byte, partyIndex int) { data, _ := json.Marshal(msg) fmt.Println(string(data)) } + +// ============================================================================= +// Signing Implementation +// ============================================================================= + +type signResult struct { + Signature []byte + R *big.Int + S *big.Int + RecoveryID int +} + +func executeSign( + sessionID, partyID string, + partyIndex, thresholdT, thresholdN int, + participants []Participant, + messageHash []byte, + shareData []byte, +) (*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() + }() + + // Deserialize keygen save data + var saveData keygen.LocalPartySaveData + if err := json.Unmarshal(shareData, &saveData); err != nil { + return nil, fmt.Errorf("failed to deserialize share data: %w", err) + } + + // Create TSS party IDs for current signers (may be subset of original keygen participants) + tssPartyIDs := make([]*tss.PartyID, len(participants)) + var selfTSSID *tss.PartyID + + for i, p := range participants { + partyKey := tss.NewPartyID( + p.PartyID, + fmt.Sprintf("party-%d", p.PartyIndex), + big.NewInt(int64(p.PartyIndex+1)), + ) + tssPartyIDs[i] = partyKey + if p.PartyID == partyID { + selfTSSID = partyKey + } + } + + if selfTSSID == nil { + return nil, fmt.Errorf("self party not found in participants") + } + + // Sort party IDs + sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) + + // Build mapping from keygen index to sorted array index + // This is needed because TSS messages use keygen index, but tssPartyIDs is sorted + keygenIndexToSortedIndex := make(map[int]int) + for sortedIdx, pid := range sortedPartyIDs { + for _, p := range participants { + if p.PartyID == pid.Id { + keygenIndexToSortedIndex[p.PartyIndex] = sortedIdx + break + } + } + } + + // Create peer context and parameters + // IMPORTANT: Use original thresholdN from keygen, not len(participants) + peerCtx := tss.NewPeerContext(sortedPartyIDs) + params := tss.NewParameters(tss.S256(), peerCtx, selfTSSID, thresholdN, thresholdT) + + // Convert message hash to big.Int + msgHashBigInt := new(big.Int).SetBytes(messageHash) + + // Create channels + signerCount := len(participants) + outCh := make(chan tss.Message, signerCount*10) + endCh := make(chan *common.SignatureData, 1) + errCh := make(chan error, 1) + + // Create local signing party + localParty := signing.NewLocalParty(msgHashBigInt, params, saveData, outCh, endCh) + + // Build party index map for incoming messages + partyIndexMap := make(map[int]*tss.PartyID) + for sortedIdx, pid := range sortedPartyIDs { + for _, p := range participants { + if p.PartyID == pid.Id { + partyIndexMap[p.PartyIndex] = sortedPartyIDs[sortedIdx] + 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) + // Increase buffer for large messages + buf := make([]byte, 1024*1024) + scanner.Buffer(buf, len(buf)) + 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" { + handleSignIncomingMessage(msg, localParty, partyIndexMap, keygenIndexToSortedIndex, sortedPartyIDs, errCh) + } + } + }() + + // Track progress + totalRounds := 9 // GG20 signing has 9 rounds + + // Wait for completion + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-errCh: + return nil, err + case signData := <-endCh: + // Signing completed successfully + sendProgress(totalRounds, totalRounds) + + // Build signature (R || S) + signature := make([]byte, 64) + rBytes := signData.R + sBytes := signData.S + copy(signature[32-len(rBytes):32], rBytes) + copy(signature[64-len(sBytes):64], sBytes) + + r := new(big.Int).SetBytes(signData.R) + s := new(big.Int).SetBytes(signData.S) + recoveryID := int(signData.SignatureRecovery[0]) + + return &signResult{ + Signature: signature, + R: r, + S: s, + RecoveryID: recoveryID, + }, nil + } +} + +func handleSignIncomingMessage( + msg Message, + localParty tss.Party, + partyIndexMap map[int]*tss.PartyID, + keygenIndexToSortedIndex map[int]int, + sortedPartyIDs []*tss.PartyID, + errCh chan error, +) { + // Map keygen index to sorted array index + sortedIndex, exists := keygenIndexToSortedIndex[msg.FromPartyIndex] + if !exists { + return + } + + if sortedIndex < 0 || sortedIndex >= len(sortedPartyIDs) { + return + } + + payload, err := base64.StdEncoding.DecodeString(msg.Payload) + if err != nil { + return + } + + parsedMsg, err := tss.ParseWireMessage(payload, sortedPartyIDs[sortedIndex], msg.IsBroadcast) + if err != nil { + return + } + + go func() { + _, err := localParty.Update(parsedMsg) + if err != nil { + // Only send fatal errors + if !isDuplicateError(err) { + errCh <- err + } + } + }() +} + +func sendSignResult(signature []byte, r, s *big.Int, recoveryID int) { + msg := Message{ + Type: "sign_result", + Signature: base64.StdEncoding.EncodeToString(signature), + R: fmt.Sprintf("%064x", r), + S: fmt.Sprintf("%064x", s), + RecoveryID: recoveryID, + } + data, _ := json.Marshal(msg) + fmt.Println(string(data)) +} diff --git a/backend/mpc-system/services/service-party-app/tss-party/tss_party_e2e_test.go b/backend/mpc-system/services/service-party-app/tss-party/tss_party_e2e_test.go new file mode 100644 index 00000000..051baf0a --- /dev/null +++ b/backend/mpc-system/services/service-party-app/tss-party/tss_party_e2e_test.go @@ -0,0 +1,463 @@ +// Package main provides E2E tests for tss-party.exe +// +// This test simulates the full flow of keygen and signing using tss-party.exe +// by spawning multiple processes and coordinating message passing between them. +package main + +import ( + "bufio" + "context" + "crypto/ecdsa" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math/big" + "os" + "os/exec" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/stretchr/testify/require" +) + +// TestTSSPartyE2E tests the full keygen and signing flow using tss-party.exe +func TestTSSPartyE2E(t *testing.T) { + // Find the tss-party.exe + exePath, err := filepath.Abs("tss-party.exe") + if err != nil { + t.Fatalf("Failed to get exe path: %v", err) + } + + // Check if exe exists + if _, err := os.Stat(exePath); os.IsNotExist(err) { + t.Skipf("tss-party.exe not found at %s, run 'go build' first", exePath) + } + + // Test parameters + sessionID := "test-session-001" + // In tss-lib: threshold=t means t+1 signers required + // For 2-of-3: we want 2 signers, so t=1 (1+1=2) + thresholdT := 1 // t=1 means 2 signers required (t+1=2) + thresholdN := 3 + password := "test-password-123" + + participants := []Participant{ + {PartyID: "party-0", PartyIndex: 0}, + {PartyID: "party-1", PartyIndex: 1}, + {PartyID: "party-2", PartyIndex: 2}, + } + + // ============================================ + // Step 1: Run Keygen + // ============================================ + t.Log("========================================") + t.Log(" Step 1: Running Keygen") + t.Log("========================================") + + keygenResults := runKeygenE2E(t, exePath, sessionID, thresholdT, thresholdN, participants, password) + require.Len(t, keygenResults, 3, "Should have 3 keygen results") + + // Verify all parties have the same public key + pubKey0 := keygenResults[0].PublicKey + for i, r := range keygenResults { + require.Equal(t, pubKey0, r.PublicKey, "Party %d should have same public key", i) + } + + t.Logf("Keygen completed! Public key: %s", hex.EncodeToString(pubKey0)) + + // ============================================ + // Step 2: Run Signing with 2 parties + // ============================================ + t.Log("========================================") + t.Log(" Step 2: Running Signing (2-of-3)") + t.Log("========================================") + + message := []byte("Hello MPC World!") + messageHash := sha256.Sum256(message) + + // Sign with parties 0 and 1 + signers := []Participant{ + participants[0], + participants[1], + } + + signerShares := [][]byte{ + keygenResults[0].EncryptedShare, + keygenResults[1].EncryptedShare, + } + + signResult := runSignE2E(t, exePath, sessionID+"-sign", thresholdT, thresholdN, signers, signerShares, messageHash[:], password) + + t.Logf("Signing completed!") + t.Logf(" R: %s", signResult.R) + t.Logf(" S: %s", signResult.S) + t.Logf(" RecoveryID: %d", signResult.RecoveryID) + + // ============================================ + // Step 3: Verify Signature + // ============================================ + t.Log("========================================") + t.Log(" Step 3: Verifying Signature") + t.Log("========================================") + + // Parse public key + pubKeyECDSA := parseCompressedPublicKey(t, pubKey0) + + // Parse R and S + rBigInt, ok := new(big.Int).SetString(signResult.R, 16) + require.True(t, ok, "Failed to parse R") + sBigInt, ok := new(big.Int).SetString(signResult.S, 16) + require.True(t, ok, "Failed to parse S") + + // Verify + valid := ecdsa.Verify(pubKeyECDSA, messageHash[:], rBigInt, sBigInt) + require.True(t, valid, "Signature verification should pass") + + t.Log("✓ Signature verified successfully!") + t.Log("========================================") + t.Log(" E2E Test PASSED!") + t.Log("========================================") +} + +type keygenE2EResult struct { + PublicKey []byte + EncryptedShare []byte +} + +type signE2EResult struct { + Signature []byte + R string + S string + RecoveryID int +} + +func runKeygenE2E(t *testing.T, exePath, sessionID string, thresholdT, thresholdN int, participants []Participant, password string) []*keygenE2EResult { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + participantsJSON, _ := json.Marshal(participants) + + // Create processes for all parties + type partyProc struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout io.ReadCloser + party Participant + } + + procs := make([]*partyProc, len(participants)) + + for i, p := range participants { + args := []string{ + "keygen", + "-session-id", sessionID, + "-party-id", p.PartyID, + "-party-index", fmt.Sprintf("%d", p.PartyIndex), + "-threshold-t", fmt.Sprintf("%d", thresholdT), + "-threshold-n", fmt.Sprintf("%d", thresholdN), + "-participants", string(participantsJSON), + "-password", password, + } + + cmd := exec.CommandContext(ctx, exePath, args...) + + stdin, err := cmd.StdinPipe() + require.NoError(t, err) + + stdout, err := cmd.StdoutPipe() + require.NoError(t, err) + + cmd.Stderr = os.Stderr + + err = cmd.Start() + require.NoError(t, err) + + procs[i] = &partyProc{ + cmd: cmd, + stdin: stdin, + stdout: stdout, + party: p, + } + } + + // Message router + type routedMsg struct { + fromIndex int + isBroadcast bool + toParties []string + payload string + } + msgChan := make(chan routedMsg, 100) + + // Result channel + results := make([]*keygenE2EResult, len(participants)) + var resultsMu sync.Mutex + + // Read output from all processes + var wg sync.WaitGroup + for i, proc := range procs { + wg.Add(1) + go func(idx int, p *partyProc) { + defer wg.Done() + scanner := bufio.NewScanner(p.stdout) + buf := make([]byte, 1024*1024) + scanner.Buffer(buf, len(buf)) + + for scanner.Scan() { + var msg Message + if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil { + continue + } + + switch msg.Type { + case "outgoing": + msgChan <- routedMsg{ + fromIndex: p.party.PartyIndex, + isBroadcast: msg.IsBroadcast, + toParties: msg.ToParties, + payload: msg.Payload, + } + case "result": + pubKey, _ := base64.StdEncoding.DecodeString(msg.PublicKey) + share, _ := base64.StdEncoding.DecodeString(msg.EncryptedShare) + resultsMu.Lock() + results[idx] = &keygenE2EResult{ + PublicKey: pubKey, + EncryptedShare: share, + } + resultsMu.Unlock() + case "progress": + t.Logf("Party %d: Round %d/%d", idx, msg.Round, msg.TotalRounds) + case "error": + t.Errorf("Party %d error: %s", idx, msg.Error) + } + } + }(i, proc) + } + + // Route messages between processes + go func() { + for { + select { + case <-ctx.Done(): + return + case m := <-msgChan: + incomingMsg := Message{ + Type: "incoming", + IsBroadcast: m.isBroadcast, + Payload: m.payload, + FromPartyIndex: m.fromIndex, + } + data, _ := json.Marshal(incomingMsg) + dataLine := string(data) + "\n" + + if m.isBroadcast { + // Send to all other parties + for idx, proc := range procs { + if idx != m.fromIndex { + proc.stdin.Write([]byte(dataLine)) + } + } + } else { + // Send to specific parties + for _, toPartyID := range m.toParties { + for _, proc := range procs { + if proc.party.PartyID == toPartyID { + proc.stdin.Write([]byte(dataLine)) + break + } + } + } + } + } + } + }() + + // Wait for all processes to complete + for _, proc := range procs { + proc.cmd.Wait() + } + + return results +} + +func runSignE2E(t *testing.T, exePath, sessionID string, thresholdT, thresholdN int, participants []Participant, shares [][]byte, messageHash []byte, password string) *signE2EResult { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + participantsJSON, _ := json.Marshal(participants) + messageHashB64 := base64.StdEncoding.EncodeToString(messageHash) + + // Create processes for all signers + type partyProc struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout io.ReadCloser + party Participant + } + + procs := make([]*partyProc, len(participants)) + + for i, p := range participants { + // Decrypt share data (remove password hash prefix) + shareData := shares[i][32:] // Skip the 32-byte password hash + shareDataB64 := base64.StdEncoding.EncodeToString(shareData) + + args := []string{ + "sign", + "-session-id", sessionID, + "-party-id", p.PartyID, + "-party-index", fmt.Sprintf("%d", p.PartyIndex), + "-threshold-t", fmt.Sprintf("%d", thresholdT), + "-threshold-n", fmt.Sprintf("%d", thresholdN), + "-participants", string(participantsJSON), + "-message-hash", messageHashB64, + "-share-data", shareDataB64, + } + + cmd := exec.CommandContext(ctx, exePath, args...) + + stdin, err := cmd.StdinPipe() + require.NoError(t, err) + + stdout, err := cmd.StdoutPipe() + require.NoError(t, err) + + cmd.Stderr = os.Stderr + + err = cmd.Start() + require.NoError(t, err) + + procs[i] = &partyProc{ + cmd: cmd, + stdin: stdin, + stdout: stdout, + party: p, + } + } + + // Message router + type routedMsg struct { + fromIndex int + isBroadcast bool + toParties []string + payload string + } + msgChan := make(chan routedMsg, 100) + + // Result channel + var result *signE2EResult + var resultMu sync.Mutex + resultChan := make(chan struct{}) + + // Read output from all processes + var wg sync.WaitGroup + for i, proc := range procs { + wg.Add(1) + go func(idx int, p *partyProc) { + defer wg.Done() + scanner := bufio.NewScanner(p.stdout) + buf := make([]byte, 1024*1024) + scanner.Buffer(buf, len(buf)) + + for scanner.Scan() { + var msg Message + if err := json.Unmarshal(scanner.Bytes(), &msg); err != nil { + continue + } + + switch msg.Type { + case "outgoing": + msgChan <- routedMsg{ + fromIndex: p.party.PartyIndex, + isBroadcast: msg.IsBroadcast, + toParties: msg.ToParties, + payload: msg.Payload, + } + case "sign_result": + sig, _ := base64.StdEncoding.DecodeString(msg.Signature) + resultMu.Lock() + if result == nil { + result = &signE2EResult{ + Signature: sig, + R: msg.R, + S: msg.S, + RecoveryID: msg.RecoveryID, + } + close(resultChan) + } + resultMu.Unlock() + case "progress": + t.Logf("Party %d: Round %d/%d", idx, msg.Round, msg.TotalRounds) + case "error": + t.Errorf("Party %d error: %s", idx, msg.Error) + } + } + }(i, proc) + } + + // Route messages between processes + go func() { + for { + select { + case <-ctx.Done(): + return + case m := <-msgChan: + incomingMsg := Message{ + Type: "incoming", + IsBroadcast: m.isBroadcast, + Payload: m.payload, + FromPartyIndex: m.fromIndex, + } + data, _ := json.Marshal(incomingMsg) + dataLine := string(data) + "\n" + + if m.isBroadcast { + // Send to all other parties + for idx, proc := range procs { + if idx != m.fromIndex { + proc.stdin.Write([]byte(dataLine)) + } + } + } else { + // Send to specific parties + for _, toPartyID := range m.toParties { + for _, proc := range procs { + if proc.party.PartyID == toPartyID { + proc.stdin.Write([]byte(dataLine)) + break + } + } + } + } + } + } + }() + + // Wait for result or timeout + select { + case <-resultChan: + case <-ctx.Done(): + t.Fatal("Signing timed out") + } + + // Wait for all processes to complete + for _, proc := range procs { + proc.cmd.Wait() + } + + return result +} + +func parseCompressedPublicKey(t *testing.T, compressed []byte) *ecdsa.PublicKey { + pubKey, err := btcec.ParsePubKey(compressed) + require.NoError(t, err) + + return pubKey.ToECDSA() +}