feat(service-party-app): implement TSS signing protocol
- Add sign command to tss-party.exe with GG20 9-round signing protocol - Add participateSign() method to TSSHandler for TypeScript integration - Add grpc:executeSign IPC handler in main.ts - Add kava_evm chain config for EVM address derivation - Fix ArrayBuffer type handling in Home.tsx export - Add E2E test for keygen + signing flow (tss_party_e2e_test.go) The signing implementation: - Uses keygen share data (LocalPartySaveData) for signing - Supports threshold signing (t-of-n with subset of parties) - Returns signature (R || S), recovery ID for ecrecover - Verified with ECDSA signature verification in E2E test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2e718aecfa
commit
bc78ab3fac
|
|
@ -24,7 +24,7 @@ const USE_MOCK_TSS = process.env.USE_MOCK_TSS === 'true';
|
||||||
let debugLogEnabled = false;
|
let debugLogEnabled = false;
|
||||||
|
|
||||||
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
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) {
|
function sendDebugLog(level: LogLevel, source: LogSource, message: string) {
|
||||||
if (debugLogEnabled && mainWindow) {
|
if (debugLogEnabled && mainWindow) {
|
||||||
|
|
@ -1080,7 +1080,7 @@ function setupIpcHandlers() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC - 加入签名会话
|
// gRPC - 加入签名会话并执行签名协议
|
||||||
ipcMain.handle('grpc:joinSigningSession', async (_event, params) => {
|
ipcMain.handle('grpc:joinSigningSession', async (_event, params) => {
|
||||||
try {
|
try {
|
||||||
// 从本地 SQLite 获取 share 数据
|
// 从本地 SQLite 获取 share 数据
|
||||||
|
|
@ -1089,19 +1089,121 @@ function setupIpcHandlers() {
|
||||||
return { success: false, error: 'Share not found or incorrect password' };
|
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)
|
// 加入签名会话 (通过 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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
partyId: share.party_id,
|
partyId: share.party_id,
|
||||||
partyIndex: share.party_index,
|
partyIndex: share.party_index,
|
||||||
|
sessionInfo: joinResult.session_info,
|
||||||
|
otherParties: joinResult.other_parties,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: (error as Error).message };
|
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)
|
// Account 服务相关 (HTTP API)
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
@ -1222,8 +1324,9 @@ function setupIpcHandlers() {
|
||||||
participants: share.participants || [],
|
participants: share.participants || [],
|
||||||
}, password);
|
}, password);
|
||||||
|
|
||||||
// 自动派生 Kava 地址
|
// 自动派生地址 (Kava Cosmos + Kava EVM)
|
||||||
if (saved && share.publicKey) {
|
if (saved && share.publicKey) {
|
||||||
|
// 派生 Kava Cosmos 地址
|
||||||
try {
|
try {
|
||||||
const kavaAddress = addressDerivationService.deriveAddress(share.publicKey, 'kava');
|
const kavaAddress = addressDerivationService.deriveAddress(share.publicKey, 'kava');
|
||||||
database?.saveDerivedAddress({
|
database?.saveDerivedAddress({
|
||||||
|
|
@ -1236,6 +1339,20 @@ function setupIpcHandlers() {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to derive Kava address:', 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 };
|
return { success: true, data: saved };
|
||||||
|
|
@ -1606,6 +1723,18 @@ function setupIpcHandlers() {
|
||||||
return result.canceled ? null : result.filePath;
|
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 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// 调试相关
|
// 调试相关
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ export const CHAIN_CONFIGS: Record<string, ChainConfig> = {
|
||||||
curve: 'secp256k1',
|
curve: 'secp256k1',
|
||||||
derivationPath: "m/44'/459'/0'/0/0",
|
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: {
|
cosmos: {
|
||||||
name: 'Cosmos Hub',
|
name: 'Cosmos Hub',
|
||||||
prefix: 'cosmos',
|
prefix: 'cosmos',
|
||||||
|
|
@ -263,7 +270,8 @@ export class AddressDerivationService {
|
||||||
|
|
||||||
let address: string;
|
let address: string;
|
||||||
|
|
||||||
if (chain === 'ethereum') {
|
if (chain === 'ethereum' || chain === 'kava_evm') {
|
||||||
|
// EVM 兼容地址 (0x 格式)
|
||||||
address = deriveEthereumAddress(publicKeyHex);
|
address = deriveEthereumAddress(publicKeyHex);
|
||||||
} else if (config.curve === 'ed25519') {
|
} else if (config.curve === 'ed25519') {
|
||||||
address = deriveEd25519Address(publicKeyHex, config.prefix);
|
address = deriveEd25519Address(publicKeyHex, config.prefix);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ export interface KeygenResult {
|
||||||
export interface SignResult {
|
export interface SignResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
signature: Buffer;
|
signature: Buffer;
|
||||||
|
r: string; // hex encoded
|
||||||
|
s: string; // hex encoded
|
||||||
|
recoveryId: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,7 +29,7 @@ export interface SignResult {
|
||||||
* TSS 消息处理器接口
|
* TSS 消息处理器接口
|
||||||
*/
|
*/
|
||||||
interface TSSMessage {
|
interface TSSMessage {
|
||||||
type: 'outgoing' | 'result' | 'error' | 'progress';
|
type: 'outgoing' | 'result' | 'sign_result' | 'error' | 'progress';
|
||||||
isBroadcast?: boolean;
|
isBroadcast?: boolean;
|
||||||
toParties?: string[];
|
toParties?: string[];
|
||||||
payload?: string; // base64 encoded
|
payload?: string; // base64 encoded
|
||||||
|
|
@ -36,6 +39,11 @@ interface TSSMessage {
|
||||||
round?: number;
|
round?: number;
|
||||||
totalRounds?: number;
|
totalRounds?: number;
|
||||||
error?: string;
|
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 = [];
|
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<SignResult> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消正在进行的协议
|
* 取消正在进行的协议
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,19 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
shareId: string;
|
shareId: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
keygenSessionId?: string;
|
||||||
|
joinToken?: string;
|
||||||
}) => ipcRenderer.invoke('grpc:joinSigningSession', params),
|
}) => 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) => {
|
subscribeSigningProgress: (sessionId: string, callback: (event: unknown) => void) => {
|
||||||
const channel = `signing:progress:${sessionId}`;
|
const channel = `signing:progress:${sessionId}`;
|
||||||
const listener = (_event: unknown, data: unknown) => callback(data);
|
const listener = (_event: unknown, data: unknown) => callback(data);
|
||||||
|
|
@ -251,6 +262,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
ipcRenderer.invoke('dialog:saveFile', { defaultPath, filters }),
|
ipcRenderer.invoke('dialog:saveFile', { defaultPath, filters }),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// 文件操作相关
|
||||||
|
// ===========================================================================
|
||||||
|
file: {
|
||||||
|
write: (filePath: string, data: Uint8Array) =>
|
||||||
|
ipcRenderer.invoke('file:write', { filePath, data }),
|
||||||
|
},
|
||||||
|
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// 调试相关
|
// 调试相关
|
||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,7 @@ interface ShareItem {
|
||||||
threshold: { t: number; n: number };
|
threshold: { t: number; n: number };
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastUsedAt?: string;
|
lastUsedAt?: string;
|
||||||
metadata: {
|
participants?: Array<{ partyId: string; name: string }>;
|
||||||
participants: Array<{ partyId: string; name: string }>;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShareWithAddress extends ShareItem {
|
interface ShareWithAddress extends ShareItem {
|
||||||
|
|
@ -89,15 +87,14 @@ export default function Home() {
|
||||||
|
|
||||||
const result = await window.electronAPI.storage.exportShare(id, password);
|
const result = await window.electronAPI.storage.exportShare(id, password);
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
// 通过 IPC 写入文件
|
// 使用 file:write IPC 写入文件到用户选择的路径
|
||||||
const blob = new Blob([result.data], { type: 'application/octet-stream' });
|
const dataArray = result.data instanceof ArrayBuffer ? new Uint8Array(result.data) : result.data;
|
||||||
const url = URL.createObjectURL(blob);
|
const writeResult = await window.electronAPI.file.write(savePath, dataArray);
|
||||||
const a = document.createElement('a');
|
if (writeResult.success) {
|
||||||
a.href = url;
|
alert('备份文件导出成功!');
|
||||||
a.download = `share-backup-${id.slice(0, 8)}.dat`;
|
} else {
|
||||||
a.click();
|
alert('写入文件失败: ' + (writeResult.error || '未知错误'));
|
||||||
URL.revokeObjectURL(url);
|
}
|
||||||
alert('备份文件导出成功!');
|
|
||||||
} else {
|
} else {
|
||||||
alert('导出失败: ' + (result.error || '未知错误'));
|
alert('导出失败: ' + (result.error || '未知错误'));
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +218,7 @@ export default function Home() {
|
||||||
<div className={styles.infoRow}>
|
<div className={styles.infoRow}>
|
||||||
<span className={styles.infoLabel}>参与方</span>
|
<span className={styles.infoLabel}>参与方</span>
|
||||||
<span className={styles.infoValue}>
|
<span className={styles.infoValue}>
|
||||||
{(share.metadata?.participants || []).length} 人
|
{(share.participants || []).length || share.threshold.n} 人
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.infoRow}>
|
<div className={styles.infoRow}>
|
||||||
|
|
|
||||||
|
|
@ -191,12 +191,37 @@ interface JoinSigningSessionParams {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
shareId: string;
|
shareId: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
keygenSessionId?: string;
|
||||||
|
joinToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface JoinSigningSessionResult {
|
interface JoinSigningSessionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
partyId?: string;
|
partyId?: string;
|
||||||
partyIndex?: number;
|
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;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -447,6 +472,7 @@ interface ElectronAPI {
|
||||||
// 签名相关
|
// 签名相关
|
||||||
validateSigningSession: (code: string) => Promise<ValidateSigningSessionResult>;
|
validateSigningSession: (code: string) => Promise<ValidateSigningSessionResult>;
|
||||||
joinSigningSession: (params: JoinSigningSessionParams) => Promise<JoinSigningSessionResult>;
|
joinSigningSession: (params: JoinSigningSessionParams) => Promise<JoinSigningSessionResult>;
|
||||||
|
executeSign: (params: ExecuteSignParams) => Promise<ExecuteSignResult>;
|
||||||
subscribeSigningProgress: (sessionId: string, callback: (event: SigningProgressEvent) => void) => () => void;
|
subscribeSigningProgress: (sessionId: string, callback: (event: SigningProgressEvent) => void) => () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -513,6 +539,11 @@ interface ElectronAPI {
|
||||||
saveFile: (defaultPath?: string, filters?: { name: string; extensions: string[] }[]) => Promise<string | null>;
|
saveFile: (defaultPath?: string, filters?: { name: string; extensions: string[] }[]) => Promise<string | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 文件操作相关
|
||||||
|
file: {
|
||||||
|
write: (filePath: string, data: Uint8Array) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
// 调试相关
|
// 调试相关
|
||||||
debug?: {
|
debug?: {
|
||||||
subscribeLogs: (callback: (event: unknown, data: { level: string; source: string; message: string }) => void) => void;
|
subscribeLogs: (callback: (event: unknown, data: { level: string; source: string; message: string }) => void) => void;
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,18 @@ module github.com/rwadurian/mpc-system/services/service-party-app/tss-party
|
||||||
|
|
||||||
go 1.21
|
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 (
|
require (
|
||||||
github.com/agl/ed25519 v0.0.0-20200225211852-fd4d107ace12 // indirect
|
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/edwards/v2 v2.0.3 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // 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/opentracing/opentracing-go v1.2.0 // indirect
|
||||||
github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11 // indirect
|
github.com/otiai10/primes v0.0.0-20210501021515-f1b2be525a11 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // 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/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.26.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
|
golang.org/x/sys v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.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
|
// Replace to fix tss-lib dependency issue with ed25519
|
||||||
|
|
|
||||||
|
|
@ -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/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.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.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 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.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
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.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
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.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/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/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 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/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/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=
|
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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
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/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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.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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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-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-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.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/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/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=
|
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 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
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 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/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"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/keygen"
|
||||||
|
"github.com/bnb-chain/tss-lib/v2/ecdsa/signing"
|
||||||
"github.com/bnb-chain/tss-lib/v2/tss"
|
"github.com/bnb-chain/tss-lib/v2/tss"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,6 +37,11 @@ type Message struct {
|
||||||
TotalRounds int `json:"totalRounds,omitempty"`
|
TotalRounds int `json:"totalRounds,omitempty"`
|
||||||
FromPartyIndex int `json:"fromPartyIndex,omitempty"`
|
FromPartyIndex int `json:"fromPartyIndex,omitempty"`
|
||||||
Error string `json:"error,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
|
// Participant info
|
||||||
|
|
@ -118,9 +125,83 @@ func runKeygen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSign() {
|
func runSign() {
|
||||||
// TODO: Implement signing
|
// Parse sign flags
|
||||||
sendError("Signing not implemented yet")
|
fs := flag.NewFlagSet("sign", flag.ExitOnError)
|
||||||
os.Exit(1)
|
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 {
|
type keygenResult struct {
|
||||||
|
|
@ -405,3 +486,237 @@ func sendResult(publicKey, encryptedShare []byte, partyIndex int) {
|
||||||
data, _ := json.Marshal(msg)
|
data, _ := json.Marshal(msg)
|
||||||
fmt.Println(string(data))
|
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))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue