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;
|
||||
|
||||
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 };
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// 调试相关
|
||||
// ===========================================================================
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ export const CHAIN_CONFIGS: Record<string, ChainConfig> = {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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<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;
|
||||
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 }),
|
||||
},
|
||||
|
||||
// ===========================================================================
|
||||
// 调试相关
|
||||
// ===========================================================================
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div className={styles.infoRow}>
|
||||
<span className={styles.infoLabel}>参与方</span>
|
||||
<span className={styles.infoValue}>
|
||||
{(share.metadata?.participants || []).length} 人
|
||||
{(share.participants || []).length || share.threshold.n} 人
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
|
|
|
|||
|
|
@ -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<ValidateSigningSessionResult>;
|
||||
joinSigningSession: (params: JoinSigningSessionParams) => Promise<JoinSigningSessionResult>;
|
||||
executeSign: (params: ExecuteSignParams) => Promise<ExecuteSignResult>;
|
||||
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>;
|
||||
};
|
||||
|
||||
// 文件操作相关
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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