fix(tss): 修复备份恢复后签名失败的问题

问题原因:
- 备份恢复的钱包在签名时使用了当前设备的 partyId,而不是原始 keygen 时的 partyId
- TSS 协议要求签名时使用的 partyId 必须与 keygen 时完全一致

修复内容:
- Android: joinSignSessionViaGrpc() 使用 shareEntity.partyId 而非当前设备 partyId
- Electron: cosign:joinSession 和 cosign:createSession 使用 share.party_id
- Electron: handleCoSignStart() 使用 share.party_id 进行签名
- 所有 gRPC 通信和消息订阅都使用原始 partyId

关键修改点:
- TssRepository.kt: joinSignSessionViaGrpc() 第 1136 行使用 signingPartyId
- main.ts: cosign:joinSession 第 1826 行使用 signingPartyId
- main.ts: cosign:createSession 第 1624-1633 行使用 share.party_id
- main.ts: handleCoSignStart() 第 836 行使用 share.party_id

其他:
- 移除 Android APK 中的 x86_64 ABI (仅用于模拟器)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-20 00:39:05 -08:00
parent c5db77d23a
commit 94d283696f
3 changed files with 61 additions and 31 deletions

View File

@ -39,8 +39,9 @@ android {
}
// NDK configuration for TSS native library
// Only include ARM ABIs for real devices (x86_64 is for emulators only)
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}

View File

@ -1126,14 +1126,26 @@ class TssRepository @Inject constructor(
// Note: Password is verified during actual sign execution, same as Electron
// CRITICAL: Use the original partyId from the share (keygen time) for signing
// This is essential for backup/restore - the partyId must match what was used during keygen
// If shareEntity.partyId is empty (legacy data), fall back to current device's partyId
val signingPartyId = if (shareEntity.partyId.isNotEmpty()) {
shareEntity.partyId
} else {
android.util.Log.w("TssRepository", "Share has no partyId (legacy data), using current device partyId")
partyId
}
currentSigningPartyId = signingPartyId // Save for later use in this flow
android.util.Log.d("TssRepository", "Using signingPartyId=$signingPartyId (current device partyId=$partyId)")
// CRITICAL: Set pendingSessionId BEFORE joinSession to avoid race condition
// This ensures session_started events can be matched even if they arrive
// before _currentSession is set
pendingSessionId = sessionId
android.util.Log.d("TssRepository", "Set pendingSessionId=$sessionId for event matching (sign joiner)")
// Join session via gRPC (matching Electron's grpcClient.joinSession)
val joinResult = grpcClient.joinSession(sessionId, partyId, joinToken)
// Join session via gRPC using the original partyId from keygen (CRITICAL for backup/restore)
val joinResult = grpcClient.joinSession(sessionId, signingPartyId, joinToken)
if (joinResult.isFailure) {
android.util.Log.e("TssRepository", "gRPC sign join failed", joinResult.exceptionOrNull())
return@withContext Result.failure(joinResult.exceptionOrNull()!!)
@ -1148,12 +1160,13 @@ class TssRepository @Inject constructor(
// Build participants list (matching Electron's logic)
// Prefer using parties from validateInviteCode (complete list)
// CRITICAL: Use signingPartyId (original partyId from keygen) for participant identification
val participants = if (parties.isNotEmpty()) {
parties.toMutableList()
} else {
// Fallback: use other_parties + self
val list = sessionData.participants.toMutableList()
list.add(Participant(partyId, myPartyIndex, ""))
list.add(Participant(signingPartyId, myPartyIndex, ""))
list.sortBy { it.partyIndex }
list
}

View File

@ -821,6 +821,21 @@ async function handleCoSignStart(event: {
// 标记签名开始
signInProgressSessionId = event.sessionId;
// CRITICAL: Get the original partyId from keygen (stored in share) for signing
// This is essential for backup/restore - the partyId must match what was used during keygen
const share = database?.getShare(activeCoSignSession.shareId, activeCoSignSession.sharePassword);
if (!share) {
debugLog.error('main', 'Failed to get share data');
mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, {
type: 'failed',
error: 'Failed to get share data',
});
signInProgressSessionId = null;
return;
}
const signingPartyId = share.party_id || grpcClient?.getPartyId() || '';
debugLog.info('main', `Using signingPartyId=${signingPartyId} (currentDevicePartyId=${grpcClient?.getPartyId()})`);
// 打印当前 activeCoSignSession.participants 状态
console.log('[CO-SIGN] Current activeCoSignSession.participants before update:',
activeCoSignSession.participants.map(p => ({
@ -832,8 +847,9 @@ async function handleCoSignStart(event: {
// 从 event.selectedParties 更新参与者列表
// 优先使用 activeCoSignSession.participants 中的 partyIndex来自 signingParties 或 other_parties
// CRITICAL: Use signingPartyId (original from keygen) for identification
if (event.selectedParties && event.selectedParties.length > 0) {
const myPartyId = grpcClient?.getPartyId();
const myPartyId = signingPartyId;
const updatedParticipants: Array<{ partyId: string; partyIndex: number; name: string }> = [];
event.selectedParties.forEach((partyId) => {
@ -869,21 +885,11 @@ async function handleCoSignStart(event: {
})));
}
// 获取 share 数据
const share = database?.getShare(activeCoSignSession.shareId, activeCoSignSession.sharePassword);
if (!share) {
debugLog.error('main', 'Failed to get share data');
mainWindow?.webContents.send(`cosign:events:${event.sessionId}`, {
type: 'failed',
error: 'Failed to get share data',
});
signInProgressSessionId = null;
return;
}
// Note: share already fetched above for getting signingPartyId
console.log('[CO-SIGN] Calling tssHandler.participateSign with:', {
sessionId: activeCoSignSession.sessionId,
partyId: grpcClient?.getPartyId(),
partyId: signingPartyId, // CRITICAL: Use signingPartyId (original from keygen)
partyIndex: activeCoSignSession.partyIndex,
participants: activeCoSignSession.participants.map(p => ({ partyId: p.partyId.substring(0, 8), partyIndex: p.partyIndex })),
threshold: activeCoSignSession.threshold,
@ -892,9 +898,10 @@ async function handleCoSignStart(event: {
debugLog.info('tss', `Starting sign for session ${event.sessionId}...`);
try {
// CRITICAL: Use signingPartyId (original partyId from keygen) for signing
const result = await (tssHandler as TSSHandler).participateSign(
activeCoSignSession.sessionId,
grpcClient?.getPartyId() || '',
signingPartyId, // CRITICAL: Use original partyId from keygen for backup/restore to work
activeCoSignSession.partyIndex,
activeCoSignSession.participants,
activeCoSignSession.threshold,
@ -1613,9 +1620,9 @@ function setupIpcHandlers() {
initiatorName?: string;
}) => {
try {
// 获取当前 party ID
const partyId = grpcClient?.getPartyId();
if (!partyId) {
// 获取当前 party ID (用于检查连接状态)
const currentDevicePartyId = grpcClient?.getPartyId();
if (!currentDevicePartyId) {
return { success: false, error: '请先连接到消息路由器' };
}
@ -1625,6 +1632,11 @@ function setupIpcHandlers() {
return { success: false, error: 'Share 不存在或密码错误' };
}
// CRITICAL: Use the original partyId from keygen (stored in share) for signing
// This is essential for backup/restore - the partyId must match what was used during keygen
const partyId = share.party_id || currentDevicePartyId;
debugLog.info('main', `Initiator using partyId=${partyId} (currentDevicePartyId=${currentDevicePartyId})`);
// 从后端获取 keygen 会话的参与者信息(包含正确的 party_index
const keygenStatus = await accountClient?.getSessionStatus(share.session_id);
if (!keygenStatus?.participants || keygenStatus.participants.length === 0) {
@ -1810,8 +1822,8 @@ function setupIpcHandlers() {
parties?: Array<{ party_id: string; party_index: number }>;
}) => {
try {
const partyId = grpcClient?.getPartyId();
if (!partyId) {
const currentDevicePartyId = grpcClient?.getPartyId();
if (!currentDevicePartyId) {
return { success: false, error: '请先连接到消息路由器' };
}
@ -1821,9 +1833,12 @@ function setupIpcHandlers() {
return { success: false, error: 'Share 不存在或密码错误' };
}
debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, partyId=${partyId}`);
// CRITICAL: Use the original partyId from keygen (stored in share) for signing
// This is essential for backup/restore - the partyId must match what was used during keygen
const signingPartyId = share.party_id || currentDevicePartyId;
debugLog.info('grpc', `Joining co-sign session: sessionId=${params.sessionId}, signingPartyId=${signingPartyId} (currentDevicePartyId=${currentDevicePartyId})`);
const result = await grpcClient?.joinSession(params.sessionId, partyId, params.joinToken);
const result = await grpcClient?.joinSession(params.sessionId, signingPartyId, params.joinToken);
if (result?.success) {
// 设置活跃的 Co-Sign 会话
// 优先使用 params.parties来自 validateInviteCode包含所有预期参与者
@ -1832,10 +1847,11 @@ function setupIpcHandlers() {
if (params.parties && params.parties.length > 0) {
// 使用完整的 parties 列表
// CRITICAL: Use signingPartyId (original from keygen) for identification
participants = params.parties.map(p => ({
partyId: p.party_id,
partyIndex: p.party_index,
name: p.party_id === partyId ? '我' : `参与方 ${p.party_index + 1}`,
name: p.party_id === signingPartyId ? '我' : `参与方 ${p.party_index + 1}`,
}));
console.log('[CO-SIGN] Participant using params.parties (complete list):', participants.map(p => ({
partyId: p.partyId.substring(0, 8),
@ -1850,9 +1866,9 @@ function setupIpcHandlers() {
name: `参与方 ${idx + 1}`,
})) || [];
// 添加自己
// 添加自己 - CRITICAL: Use signingPartyId (original from keygen)
participants.push({
partyId: partyId,
partyId: signingPartyId,
partyIndex: result.party_index,
name: '我',
});
@ -1886,11 +1902,11 @@ function setupIpcHandlers() {
messageHash: params.messageHash,
});
// 预订阅消息流
// 预订阅消息流 - CRITICAL: Use signingPartyId (original from keygen)
if (tssHandler && 'prepareForSign' in tssHandler) {
try {
debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}`);
(tssHandler as TSSHandler).prepareForSign(params.sessionId, partyId);
debugLog.info('tss', `Preparing for sign: subscribing to messages for session ${params.sessionId}, signingPartyId=${signingPartyId}`);
(tssHandler as TSSHandler).prepareForSign(params.sessionId, signingPartyId);
} catch (prepareErr) {
debugLog.error('tss', `Failed to prepare for sign: ${(prepareErr as Error).message}`);
return { success: false, error: `消息订阅失败: ${(prepareErr as Error).message}` };