From 625f9373a7e9e61ebd20ac9e455c1f940fa87e14 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 30 Dec 2025 05:35:22 -0800 Subject: [PATCH] feat(service-party-app): add KAVA transfer functionality with multi-party MPC signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Transfer page with multi-step flow (form → confirm → signing → broadcasting → success) - Implement EVM transaction building with RLP encoding (no external dependencies) - Add Keccak-256 hashing for transaction hash computation - Support EIP-155 transaction signing for Kava Testnet (Chain ID 2221) - Add transfer button to Home page wallet cards - Integrate with Account Service for creating sign sessions - Support broadcasting signed transactions to Kava EVM RPC 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 7 +- .../services/service-party-app/src/App.tsx | 2 + .../src/pages/Home.module.css | 10 + .../service-party-app/src/pages/Home.tsx | 6 + .../src/pages/Transfer.module.css | 409 ++++++++++++++ .../service-party-app/src/pages/Transfer.tsx | 513 ++++++++++++++++++ .../src/utils/transaction.ts | 393 ++++++++++++++ .../tss-party/debug_partyid.go | 60 ++ .../tss-party/test_keygen.go | 298 ++++++++++ 9 files changed, 1697 insertions(+), 1 deletion(-) create mode 100644 backend/mpc-system/services/service-party-app/src/pages/Transfer.module.css create mode 100644 backend/mpc-system/services/service-party-app/src/pages/Transfer.tsx create mode 100644 backend/mpc-system/services/service-party-app/src/utils/transaction.ts create mode 100644 backend/mpc-system/services/service-party-app/tss-party/debug_partyid.go create mode 100644 backend/mpc-system/services/service-party-app/tss-party/test_keygen.go diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2a1d04f4..85e61de3 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -559,7 +559,12 @@ "Bash(dir backendmpc-systemgithub.com /s /b)", "Bash(git status:*)", "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(service-party-app\\): 动态计算 persistent_count 并修复 keygen 触发时机\n\n1. 动态计算 server-party 数量: persistent = n - t\n - 2-of-3 -> persistent=1, external=2\n - 3-of-5 -> persistent=2, external=3\n - 4-of-7 -> persistent=3, external=4\n\n2. 修复 5 分钟超时与 24 小时会话的冲突\n - 之前: joinSession 后立即启动 5 分钟轮询,导致超时失败\n - 现在: 等待 all_joined 事件后才启动 5 分钟倒计时\n - 用户可以在 24 小时内慢慢邀请其他参与者加入\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", - "Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\")" + "Bash(dir /s /b \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\")", + "Bash(go test:*)", + "Bash(go run:*)", + "Bash(pkill:*)", + "Bash(timeout 120 go run:*)", + "Bash(timeout 60 go run:*)" ], "deny": [], "ask": [] diff --git a/backend/mpc-system/services/service-party-app/src/App.tsx b/backend/mpc-system/services/service-party-app/src/App.tsx index 0da8c865..cefa1fc6 100644 --- a/backend/mpc-system/services/service-party-app/src/App.tsx +++ b/backend/mpc-system/services/service-party-app/src/App.tsx @@ -8,6 +8,7 @@ import Join from './pages/Join'; import Create from './pages/Create'; import Session from './pages/Session'; import Sign from './pages/Sign'; +import Transfer from './pages/Transfer'; import Settings from './pages/Settings'; function App() { @@ -43,6 +44,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/backend/mpc-system/services/service-party-app/src/pages/Home.module.css b/backend/mpc-system/services/service-party-app/src/pages/Home.module.css index 8f1f352e..f14b820c 100644 --- a/backend/mpc-system/services/service-party-app/src/pages/Home.module.css +++ b/backend/mpc-system/services/service-party-app/src/pages/Home.module.css @@ -214,6 +214,16 @@ background-color: rgba(0, 90, 156, 0.05); } +.transferButton { + color: #28a745; + margin-right: var(--spacing-sm); +} + +.transferButton:hover { + border-color: #28a745; + background-color: rgba(40, 167, 69, 0.05); +} + .dangerButton { color: #dc3545; margin-left: var(--spacing-sm); 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 8dc98ed1..739eba40 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 @@ -306,6 +306,12 @@ export default function Home() { )}
+ +

转账 KAVA

+ + + {/* 钱包信息 */} + {share && ( +
+
{share.walletName}
+
+ {formatAddress(share.evmAddress || '')} +
+
+ 余额: {share.balance} KAVA +
+
+ )} + + {/* Step 1: 表单 */} + {step === 'form' && ( +
+
+ + setToAddress(e.target.value)} + /> +
+ +
+ + setAmount(e.target.value)} + step="0.000001" + min="0" + /> + +
+ +
+ + setPassword(e.target.value)} + /> +
+ + {error &&
{error}
} + + +
+ )} + + {/* Step 2: 确认 */} + {step === 'confirm' && txParams && ( +
+

确认交易

+ +
+
+ 收款地址 + {formatAddress(toAddress)} +
+
+ 转账金额 + {amount} KAVA +
+
+ Gas 费用 + {gasFeeKava} KAVA +
+
+ 总计 + + {weiToKava(kavaToWei(amount) + gasFee)} KAVA + +
+
+ +
+ 此交易需要 {share?.threshold.t} 个参与方共同签名才能完成。 + 点击"发起签名"后,您将获得邀请码,请分享给其他参与方。 +
+ + {error &&
{error}
} + +
+ + +
+
+ )} + + {/* Step 3: 签名中 */} + {step === 'signing' && ( +
+

等待多方签名

+ + {inviteCode && ( +
+

请将以下邀请码分享给其他参与方:

+
{inviteCode}
+ +
+ )} + +
+

需要 {share?.threshold.t} 个参与方签名

+

当其他参与方加入后,签名将自动开始

+
+ +
+ +
+ + {error &&
{error}
} +
+ )} + + {/* Step 4: 广播中 */} + {step === 'broadcasting' && ( +
+
+

正在广播交易...

+

请稍候,交易正在提交到 Kava 网络

+
+ )} + + {/* Step 5: 成功 */} + {step === 'success' && ( +
+
+

交易已提交

+ +
+ 交易哈希: + {txHash} +
+ +
+ + 在区块浏览器查看 + + +
+
+ )} + + {/* Step 6: 错误 */} + {step === 'error' && ( +
+
+

交易失败

+

{error}

+ +
+ +
+
+ )} +
+ ); +} diff --git a/backend/mpc-system/services/service-party-app/src/utils/transaction.ts b/backend/mpc-system/services/service-party-app/src/utils/transaction.ts new file mode 100644 index 00000000..36843257 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/src/utils/transaction.ts @@ -0,0 +1,393 @@ +/** + * EVM 交易构建和签名工具 + * 用于构建 Kava EVM 交易并计算交易哈希 + */ + +// Kava Testnet Chain ID +export const KAVA_TESTNET_CHAIN_ID = 2221; +export const KAVA_TESTNET_RPC = 'https://evm.testnet.kava.io'; + +/** + * 交易参数 + */ +export interface TransactionParams { + to: string; // 收款地址 + value: bigint; // 转账金额 (wei) + nonce: number; // 交易序号 + gasPrice: bigint; // Gas 价格 (wei) + gasLimit: bigint; // Gas 限制 + chainId: number; // 链 ID + data?: string; // 合约调用数据 (可选) +} + +/** + * 签名后的交易 + */ +export interface SignedTransaction { + rawTransaction: string; // 0x 开头的 hex 编码交易 + hash: string; // 交易哈希 +} + +/** + * 将数字转换为 RLP 编码的 bytes + */ +function numberToRlpBytes(n: bigint | number): Uint8Array { + if (n === 0 || n === 0n) { + return new Uint8Array(0); + } + + const bn = typeof n === 'number' ? BigInt(n) : n; + const hex = bn.toString(16); + const paddedHex = hex.length % 2 ? '0' + hex : hex; + const bytes = new Uint8Array(paddedHex.length / 2); + for (let i = 0; i < paddedHex.length; i += 2) { + bytes[i / 2] = parseInt(paddedHex.substring(i, i + 2), 16); + } + return bytes; +} + +/** + * 将 hex 字符串转换为 bytes + */ +function hexToBytes(hex: string): Uint8Array { + const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex; + if (cleanHex.length === 0) return new Uint8Array(0); + const bytes = new Uint8Array(cleanHex.length / 2); + for (let i = 0; i < cleanHex.length; i += 2) { + bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16); + } + return bytes; +} + +/** + * 将 bytes 转换为 hex 字符串 + */ +function bytesToHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +/** + * RLP 编码单个项目 + */ +function rlpEncodeItem(item: Uint8Array): Uint8Array { + if (item.length === 1 && item[0] < 0x80) { + // 单字节且值小于 0x80,直接返回 + return item; + } else if (item.length <= 55) { + // 短字符串: 0x80 + length + const result = new Uint8Array(1 + item.length); + result[0] = 0x80 + item.length; + result.set(item, 1); + return result; + } else { + // 长字符串: 0xb7 + lengthOfLength + length + data + const lengthBytes = numberToRlpBytes(BigInt(item.length)); + const result = new Uint8Array(1 + lengthBytes.length + item.length); + result[0] = 0xb7 + lengthBytes.length; + result.set(lengthBytes, 1); + result.set(item, 1 + lengthBytes.length); + return result; + } +} + +/** + * RLP 编码列表 + */ +function rlpEncodeList(items: Uint8Array[]): Uint8Array { + // 先编码每个项目 + const encodedItems = items.map(rlpEncodeItem); + + // 计算总长度 + const totalLength = encodedItems.reduce((sum, item) => sum + item.length, 0); + + if (totalLength <= 55) { + // 短列表: 0xc0 + length + items + const result = new Uint8Array(1 + totalLength); + result[0] = 0xc0 + totalLength; + let offset = 1; + for (const item of encodedItems) { + result.set(item, offset); + offset += item.length; + } + return result; + } else { + // 长列表: 0xf7 + lengthOfLength + length + items + const lengthBytes = numberToRlpBytes(BigInt(totalLength)); + const result = new Uint8Array(1 + lengthBytes.length + totalLength); + result[0] = 0xf7 + lengthBytes.length; + result.set(lengthBytes, 1); + let offset = 1 + lengthBytes.length; + for (const item of encodedItems) { + result.set(item, offset); + offset += item.length; + } + return result; + } +} + +/** + * Keccak-256 哈希 (与 address.ts 中相同的实现) + */ +async function keccak256(data: Uint8Array): Promise { + const RC = [ + 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, + 0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n, + 0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an, + 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an, + 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, + 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n, + 0x000000000000800an, 0x800000008000000an, 0x8000000080008081n, + 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n, + ]; + + const ROTC = [ + [0, 36, 3, 41, 18], + [1, 44, 10, 45, 2], + [62, 6, 43, 15, 61], + [28, 55, 25, 21, 56], + [27, 20, 39, 8, 14], + ]; + + function rotl64(x: bigint, n: number): bigint { + return ((x << BigInt(n)) | (x >> BigInt(64 - n))) & 0xffffffffffffffffn; + } + + function keccakF(state: bigint[][]): void { + for (let round = 0; round < 24; round++) { + const C: bigint[] = []; + for (let x = 0; x < 5; x++) { + C[x] = state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4]; + } + const D: bigint[] = []; + for (let x = 0; x < 5; x++) { + D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1); + } + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { + state[x][y] ^= D[x]; + } + } + + const B: bigint[][] = Array(5).fill(null).map(() => Array(5).fill(0n)); + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { + B[y][(2 * x + 3 * y) % 5] = rotl64(state[x][y], ROTC[x][y]); + } + } + + for (let x = 0; x < 5; x++) { + for (let y = 0; y < 5; y++) { + state[x][y] = B[x][y] ^ (~B[(x + 1) % 5][y] & B[(x + 2) % 5][y]); + } + } + + state[0][0] ^= RC[round]; + } + } + + const rate = 136; + const outputLen = 32; + + const state: bigint[][] = Array(5).fill(null).map(() => Array(5).fill(0n)); + + const padded = new Uint8Array(Math.ceil((data.length + 1) / rate) * rate); + padded.set(data); + padded[data.length] = 0x01; + padded[padded.length - 1] |= 0x80; + + for (let i = 0; i < padded.length; i += rate) { + for (let j = 0; j < rate && i + j < padded.length; j += 8) { + const x = Math.floor(j / 8) % 5; + const y = Math.floor(Math.floor(j / 8) / 5); + let lane = 0n; + for (let k = 0; k < 8 && i + j + k < padded.length; k++) { + lane |= BigInt(padded[i + j + k]) << BigInt(k * 8); + } + state[x][y] ^= lane; + } + keccakF(state); + } + + const output = new Uint8Array(outputLen); + for (let i = 0; i < outputLen; i += 8) { + const x = Math.floor(i / 8) % 5; + const y = Math.floor(Math.floor(i / 8) / 5); + const lane = state[x][y]; + for (let k = 0; k < 8 && i + k < outputLen; k++) { + output[i + k] = Number((lane >> BigInt(k * 8)) & 0xffn); + } + } + + return output; +} + +/** + * 构建未签名交易的 RLP 编码 (EIP-155) + * 用于计算签名哈希 + */ +export function buildUnsignedTxRlp(tx: TransactionParams): Uint8Array { + const items = [ + numberToRlpBytes(BigInt(tx.nonce)), + numberToRlpBytes(tx.gasPrice), + numberToRlpBytes(tx.gasLimit), + hexToBytes(tx.to), + numberToRlpBytes(tx.value), + hexToBytes(tx.data || ''), + numberToRlpBytes(BigInt(tx.chainId)), // v = chainId for EIP-155 + new Uint8Array(0), // r = 0 + new Uint8Array(0), // s = 0 + ]; + + return rlpEncodeList(items); +} + +/** + * 计算交易签名哈希 (EIP-155) + */ +export async function getTransactionHash(tx: TransactionParams): Promise { + const rlpEncoded = buildUnsignedTxRlp(tx); + const hash = await keccak256(rlpEncoded); + return '0x' + bytesToHex(hash); +} + +/** + * 构建签名后的交易 RLP 编码 + */ +export function buildSignedTxRlp( + tx: TransactionParams, + signature: { r: string; s: string; recoveryId: number } +): Uint8Array { + // EIP-155: v = chainId * 2 + 35 + recoveryId + const v = BigInt(tx.chainId) * 2n + 35n + BigInt(signature.recoveryId); + + const items = [ + numberToRlpBytes(BigInt(tx.nonce)), + numberToRlpBytes(tx.gasPrice), + numberToRlpBytes(tx.gasLimit), + hexToBytes(tx.to), + numberToRlpBytes(tx.value), + hexToBytes(tx.data || ''), + numberToRlpBytes(v), + hexToBytes(signature.r), + hexToBytes(signature.s), + ]; + + return rlpEncodeList(items); +} + +/** + * 获取账户的 nonce + */ +export async function getNonce(address: string): Promise { + const response = await fetch(KAVA_TESTNET_RPC, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_getTransactionCount', + params: [address, 'latest'], + id: 1, + }), + }); + + const data = await response.json(); + return parseInt(data.result, 16); +} + +/** + * 获取当前 gas 价格 + */ +export async function getGasPrice(): Promise { + const response = await fetch(KAVA_TESTNET_RPC, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_gasPrice', + params: [], + id: 1, + }), + }); + + const data = await response.json(); + return BigInt(data.result); +} + +/** + * 广播已签名交易 + */ +export async function broadcastTransaction(signedTxHex: string): Promise { + const response = await fetch(KAVA_TESTNET_RPC, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_sendRawTransaction', + params: [signedTxHex], + id: 1, + }), + }); + + const data = await response.json(); + if (data.error) { + throw new Error(data.error.message || 'Transaction broadcast failed'); + } + return data.result; // 返回交易哈希 +} + +/** + * 将 KAVA 数量转换为 wei + */ +export function kavaToWei(kava: string | number): bigint { + const kavaNum = typeof kava === 'string' ? parseFloat(kava) : kava; + // 使用字符串处理以避免精度问题 + const [intPart, decPart = ''] = kavaNum.toString().split('.'); + const paddedDec = decPart.padEnd(18, '0').slice(0, 18); + return BigInt(intPart + paddedDec); +} + +/** + * 将 wei 转换为 KAVA 数量 + */ +export function weiToKava(wei: bigint): string { + const weiStr = wei.toString().padStart(19, '0'); + const intPart = weiStr.slice(0, -18) || '0'; + const decPart = weiStr.slice(-18).replace(/0+$/, ''); + return decPart ? `${intPart}.${decPart}` : intPart; +} + +/** + * 验证 EVM 地址格式 + */ +export function isValidAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/.test(address); +} + +/** + * 估算转账 gas + */ +export async function estimateGas(from: string, to: string, value: bigint): Promise { + const response = await fetch(KAVA_TESTNET_RPC, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 'eth_estimateGas', + params: [{ + from, + to, + value: '0x' + value.toString(16), + }], + id: 1, + }), + }); + + const data = await response.json(); + if (data.error) { + // 默认使用 21000 (标准转账) + return 21000n; + } + return BigInt(data.result); +} diff --git a/backend/mpc-system/services/service-party-app/tss-party/debug_partyid.go b/backend/mpc-system/services/service-party-app/tss-party/debug_partyid.go new file mode 100644 index 00000000..02ea333a --- /dev/null +++ b/backend/mpc-system/services/service-party-app/tss-party/debug_partyid.go @@ -0,0 +1,60 @@ +// +build ignore + +package main + +import ( + "fmt" + "math/big" + + "github.com/bnb-chain/tss-lib/v2/tss" +) + +type Participant struct { + PartyID string + PartyIndex int +} + +func main() { + participants := []Participant{ + {PartyID: "party-0", PartyIndex: 0}, + {PartyID: "party-1", PartyIndex: 1}, + {PartyID: "party-2", PartyIndex: 2}, + } + + tssPartyIDs := make([]*tss.PartyID, len(participants)) + for i, p := range participants { + tssPartyIDs[i] = tss.NewPartyID( + p.PartyID, + fmt.Sprintf("party-%d", p.PartyIndex), + big.NewInt(int64(p.PartyIndex+1)), + ) + } + + fmt.Println("Before sorting:") + for i, p := range tssPartyIDs { + fmt.Printf(" [%d] ID=%s, Key=%v\n", i, p.Id, p.Key) + } + + sortedPartyIDs := tss.SortPartyIDs(tssPartyIDs) + + fmt.Println("\nAfter sorting:") + for i, p := range sortedPartyIDs { + fmt.Printf(" [%d] ID=%s, Key=%v\n", i, p.Id, p.Key) + } + + // Build the buggy map + partyIndexMap := make(map[int]*tss.PartyID) + for _, p := range sortedPartyIDs { + for _, orig := range participants { + if orig.PartyID == p.Id { + partyIndexMap[orig.PartyIndex] = p + break + } + } + } + + fmt.Println("\nParty index map (current buggy logic):") + for idx, p := range partyIndexMap { + fmt.Printf(" FromPartyIndex %d -> PartyID %s (Key=%v)\n", idx, p.Id, p.Key) + } +} diff --git a/backend/mpc-system/services/service-party-app/tss-party/test_keygen.go b/backend/mpc-system/services/service-party-app/tss-party/test_keygen.go new file mode 100644 index 00000000..aa8d8f55 --- /dev/null +++ b/backend/mpc-system/services/service-party-app/tss-party/test_keygen.go @@ -0,0 +1,298 @@ +// +build ignore + +// Test script to verify tss-party.exe keygen with 3 parties +// Run with: go run test_keygen.go + +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "sync" + "time" +) + +// Message types for IPC +type Message struct { + Type string `json:"type"` + IsBroadcast bool `json:"isBroadcast,omitempty"` + ToParties []string `json:"toParties,omitempty"` + Payload string `json:"payload,omitempty"` + PublicKey string `json:"publicKey,omitempty"` + EncryptedShare string `json:"encryptedShare,omitempty"` + PartyIndex int `json:"partyIndex,omitempty"` + Round int `json:"round,omitempty"` + TotalRounds int `json:"totalRounds,omitempty"` + FromPartyIndex int `json:"fromPartyIndex,omitempty"` + Error string `json:"error,omitempty"` +} + +type Participant struct { + PartyID string `json:"partyId"` + PartyIndex int `json:"partyIndex"` +} + +type Party struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + partyID string + partyIndex int + outMessages chan Message + result *Message + err error + mu sync.Mutex +} + +func main() { + fmt.Println("=== TSS Party Keygen Test ===") + fmt.Println("Testing 2-of-3 keygen with 3 tss-party.exe processes") + fmt.Println() + + sessionID := "test-session-123" + participants := []Participant{ + {PartyID: "party-0", PartyIndex: 0}, + {PartyID: "party-1", PartyIndex: 1}, + {PartyID: "party-2", PartyIndex: 2}, + } + participantsJSON, _ := json.Marshal(participants) + + // Create 3 parties + parties := make([]*Party, 3) + + for i := 0; i < 3; i++ { + party := &Party{ + partyID: fmt.Sprintf("party-%d", i), + partyIndex: i, + outMessages: make(chan Message, 100), + } + + cmd := exec.Command("./tss-party.exe", + "keygen", + "--session-id", sessionID, + "--party-id", party.partyID, + "--party-index", fmt.Sprintf("%d", i), + "--threshold-t", "2", + "--threshold-n", "3", + "--participants", string(participantsJSON), + "--password", "test-password", + ) + + stdin, err := cmd.StdinPipe() + if err != nil { + fmt.Printf("Failed to get stdin for party %d: %v\n", i, err) + return + } + + stdout, err := cmd.StdoutPipe() + if err != nil { + fmt.Printf("Failed to get stdout for party %d: %v\n", i, err) + return + } + + stderr, err := cmd.StderrPipe() + if err != nil { + fmt.Printf("Failed to get stderr for party %d: %v\n", i, err) + return + } + + party.cmd = cmd + party.stdin = stdin + party.stdout = stdout + party.stderr = stderr + parties[i] = party + } + + // Start all processes + for i, party := range parties { + if err := party.cmd.Start(); err != nil { + fmt.Printf("Failed to start party %d: %v\n", i, err) + return + } + fmt.Printf("[Party %d] Started process (PID: %d)\n", i, party.cmd.Process.Pid) + } + + // Read stderr in background + for i, party := range parties { + go func(idx int, p *Party) { + scanner := bufio.NewScanner(p.stderr) + for scanner.Scan() { + fmt.Printf("[Party %d STDERR] %s\n", idx, scanner.Text()) + } + }(i, party) + } + + // Read stdout and collect outgoing messages + for i, party := range parties { + go func(idx int, p *Party) { + scanner := bufio.NewScanner(p.stdout) + // Increase buffer size for large messages + buf := make([]byte, 1024*1024) // 1MB buffer + scanner.Buffer(buf, len(buf)) + + for scanner.Scan() { + line := scanner.Text() + var msg Message + if err := json.Unmarshal([]byte(line), &msg); err != nil { + fmt.Printf("[Party %d] Non-JSON output: %s\n", idx, line[:min(100, len(line))]) + continue + } + + switch msg.Type { + case "outgoing": + fmt.Printf("[Party %d] Outgoing: broadcast=%v, toParties=%v, payloadLen=%d\n", + idx, msg.IsBroadcast, msg.ToParties, len(msg.Payload)) + p.outMessages <- msg + case "progress": + fmt.Printf("[Party %d] Progress: round %d/%d\n", idx, msg.Round, msg.TotalRounds) + case "result": + fmt.Printf("[Party %d] Got result! PublicKey len=%d\n", idx, len(msg.PublicKey)) + p.mu.Lock() + p.result = &msg + p.mu.Unlock() + case "error": + fmt.Printf("[Party %d] Error: %s\n", idx, msg.Error) + p.mu.Lock() + p.err = fmt.Errorf(msg.Error) + p.mu.Unlock() + } + } + if err := scanner.Err(); err != nil { + fmt.Printf("[Party %d] Scanner error: %v\n", idx, err) + } + }(i, party) + } + + // Message router - route messages between parties using separate goroutines per party + // Use a mutex for each party's stdin to prevent concurrent writes + stdinMutexes := make([]sync.Mutex, 3) + + for senderIdx, sender := range parties { + go func(idx int, s *Party) { + for msg := range s.outMessages { + // Route message to recipients + if msg.IsBroadcast { + // Send to all except sender - use goroutines to avoid blocking + var wg sync.WaitGroup + for receiverIdx, receiver := range parties { + if receiverIdx != idx { + wg.Add(1) + go func(rIdx int, r *Party) { + defer wg.Done() + inMsg := Message{ + Type: "incoming", + FromPartyIndex: idx, + IsBroadcast: true, + Payload: msg.Payload, + } + data, _ := json.Marshal(inMsg) + fmt.Printf("[Router] Broadcast %d -> %d (payload=%d)\n", idx, rIdx, len(msg.Payload)) + stdinMutexes[rIdx].Lock() + _, err := r.stdin.Write(append(data, '\n')) + stdinMutexes[rIdx].Unlock() + if err != nil { + fmt.Printf("[Router] Error writing to party %d: %v\n", rIdx, err) + } + }(receiverIdx, receiver) + } + } + wg.Wait() + } else { + // Send to specific parties + for _, targetID := range msg.ToParties { + for receiverIdx, receiver := range parties { + if receiver.partyID == targetID { + inMsg := Message{ + Type: "incoming", + FromPartyIndex: idx, + IsBroadcast: false, + Payload: msg.Payload, + } + data, _ := json.Marshal(inMsg) + fmt.Printf("[Router] P2P %d -> %d (%s, payload=%d)\n", idx, receiverIdx, targetID, len(msg.Payload)) + stdinMutexes[receiverIdx].Lock() + _, err := receiver.stdin.Write(append(data, '\n')) + stdinMutexes[receiverIdx].Unlock() + if err != nil { + fmt.Printf("[Router] Error writing to party %d: %v\n", receiverIdx, err) + } + } + } + } + } + } + }(senderIdx, sender) + } + + // Wait for completion with timeout + done := make(chan bool) + go func() { + for _, party := range parties { + party.cmd.Wait() + } + done <- true + }() + + select { + case <-done: + fmt.Println("\n=== All processes completed ===") + case <-time.After(5 * time.Minute): + fmt.Println("\n=== TIMEOUT after 5 minutes ===") + for _, party := range parties { + party.cmd.Process.Kill() + } + } + + // Check results + fmt.Println("\n=== Results ===") + success := true + var publicKeys []string + for i, party := range parties { + party.mu.Lock() + if party.err != nil { + fmt.Printf("[Party %d] FAILED: %v\n", i, party.err) + success = false + } else if party.result != nil { + pkLen := len(party.result.PublicKey) + if pkLen > 40 { + fmt.Printf("[Party %d] SUCCESS: PublicKey=%s...\n", i, party.result.PublicKey[:40]) + } else { + fmt.Printf("[Party %d] SUCCESS: PublicKey=%s\n", i, party.result.PublicKey) + } + publicKeys = append(publicKeys, party.result.PublicKey) + } else { + fmt.Printf("[Party %d] NO RESULT\n", i) + success = false + } + party.mu.Unlock() + } + + // Verify all public keys match + if len(publicKeys) == 3 { + if publicKeys[0] == publicKeys[1] && publicKeys[1] == publicKeys[2] { + fmt.Println("\nAll public keys match!") + } else { + fmt.Println("\nWARNING: Public keys don't match!") + success = false + } + } + + if success { + fmt.Println("\n=== TEST PASSED ===") + } else { + fmt.Println("\n=== TEST FAILED ===") + os.Exit(1) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +}