fix(tss-android): 修复备份恢复后无法签名的问题
问题原因: 备份数据中缺少 partyId 字段。恢复到新手机后,签名时使用的是新设备 生成的 partyId,而不是 keygen 时编码到 LocalPartySaveData 中的 原始 partyId,导致 TSS 签名协议无法正确匹配密钥数据而失败。 修复内容: 1. Models.kt: - ShareRecord 添加 partyId 字段 - ShareBackup 添加 partyId 字段,备份格式版本升级到 v2 - 更新 fromShareRecord() 和 toShareRecord() 方法 2. Database.kt: - ShareRecordEntity 添加 party_id 列 - 数据库版本升级到 3 3. AppModule.kt: - 添加 MIGRATION_2_3 数据库迁移脚本 4. TssRepository.kt: - 添加 currentSigningPartyId 成员变量跟踪当前签名使用的 partyId - keygen 保存时包含 partyId (3处) - 备份导入时保存原始 partyId - 签名流程使用 shareEntity.partyId 替代设备 partyId (3处) - gRPC 调用 (markPartyReady, reportCompletion) 使用原始 partyId 关键点: 签名时必须使用 keygen 时的原始 partyId,因为该 ID 被编码 到了 TSS 密钥数据结构中。现在备份会保存此关键字段,恢复后签名 将使用正确的 partyId。 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b00de68b01
commit
a47b935bce
|
|
@ -29,6 +29,9 @@ data class ShareRecordEntity(
|
|||
@ColumnInfo(name = "party_index")
|
||||
val partyIndex: Int,
|
||||
|
||||
@ColumnInfo(name = "party_id")
|
||||
val partyId: String, // The original partyId used during keygen - required for signing
|
||||
|
||||
@ColumnInfo(name = "address")
|
||||
val address: String,
|
||||
|
||||
|
|
@ -95,7 +98,7 @@ interface AppSettingDao {
|
|||
*/
|
||||
@Database(
|
||||
entities = [ShareRecordEntity::class, AppSettingEntity::class],
|
||||
version = 2,
|
||||
version = 3, // Version 3: added party_id column to share_records
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class TssDatabase : RoomDatabase() {
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@ class TssRepository @Inject constructor(
|
|||
// partyId is loaded once from database in registerParty() and cached here
|
||||
// This matches Electron's getOrCreatePartyId() pattern
|
||||
private lateinit var partyId: String
|
||||
|
||||
// currentSigningPartyId: The partyId to use for the current signing session
|
||||
// This may differ from partyId when signing with a restored wallet backup
|
||||
// CRITICAL: For backup/restore to work, signing must use the original partyId from keygen
|
||||
private var currentSigningPartyId: String? = null
|
||||
|
||||
private var messageCollectionJob: Job? = null
|
||||
private var sessionEventJob: Job? = null
|
||||
|
||||
|
|
@ -1051,6 +1057,7 @@ class TssRepository @Inject constructor(
|
|||
val address = AddressUtils.deriveEvmAddress(publicKeyBytes)
|
||||
|
||||
// Save share record (use actual thresholds and party index from backend)
|
||||
// CRITICAL: Save partyId - this is required for signing after backup/restore
|
||||
val shareEntity = ShareRecordEntity(
|
||||
sessionId = sessionId,
|
||||
publicKey = result.publicKey,
|
||||
|
|
@ -1058,6 +1065,7 @@ class TssRepository @Inject constructor(
|
|||
thresholdT = actualThresholdT,
|
||||
thresholdN = actualThresholdN,
|
||||
partyIndex = actualPartyIndex,
|
||||
partyId = partyId,
|
||||
address = address
|
||||
)
|
||||
val id = shareRecordDao.insertShare(shareEntity)
|
||||
|
|
@ -1222,10 +1230,14 @@ class TssRepository @Inject constructor(
|
|||
} else {
|
||||
messageHash
|
||||
}
|
||||
android.util.Log.d("TssRepository", "Starting TSS sign with cleanMessageHash=${cleanMessageHash.take(20)}...")
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
|
||||
// This is required for backup/restore to work - the partyId must match what was used during keygen
|
||||
val signingPartyId = shareEntity.partyId
|
||||
currentSigningPartyId = signingPartyId // Save for later use in this flow
|
||||
android.util.Log.d("TssRepository", "Starting TSS sign with cleanMessageHash=${cleanMessageHash.take(20)}..., signingPartyId=$signingPartyId")
|
||||
val startResult = tssNativeBridge.startSign(
|
||||
sessionId = sessionId,
|
||||
partyId = partyId,
|
||||
partyId = signingPartyId,
|
||||
partyIndex = partyIndex,
|
||||
thresholdT = thresholdT,
|
||||
thresholdN = shareEntity.thresholdN, // Use original N from keygen
|
||||
|
|
@ -1243,8 +1255,8 @@ class TssRepository @Inject constructor(
|
|||
// Start collecting progress from native bridge
|
||||
startProgressCollection()
|
||||
|
||||
// Mark ready
|
||||
grpcClient.markPartyReady(sessionId, partyId)
|
||||
// Mark ready - use signingPartyId (original partyId from keygen)
|
||||
grpcClient.markPartyReady(sessionId, signingPartyId)
|
||||
|
||||
// Wait for sign result
|
||||
val signResult = tssNativeBridge.waitForSignResult()
|
||||
|
|
@ -1256,14 +1268,15 @@ class TssRepository @Inject constructor(
|
|||
|
||||
val result = signResult.getOrThrow()
|
||||
|
||||
// Report completion
|
||||
// Report completion - use signingPartyId (original partyId from keygen)
|
||||
val signatureBytes = android.util.Base64.decode(result.signature, android.util.Base64.NO_WRAP)
|
||||
grpcClient.reportCompletion(sessionId, partyId, signature = signatureBytes)
|
||||
grpcClient.reportCompletion(sessionId, signingPartyId, signature = signatureBytes)
|
||||
|
||||
stopProgressCollection()
|
||||
_sessionStatus.value = SessionStatus.COMPLETED
|
||||
pendingSessionId = null // Clear pending session ID on completion
|
||||
messageCollectionJob?.cancel()
|
||||
currentSigningPartyId = null // Clear after signing completes
|
||||
|
||||
android.util.Log.d("TssRepository", "Sign as joiner completed: signature=${result.signature.take(20)}...")
|
||||
|
||||
|
|
@ -1274,6 +1287,7 @@ class TssRepository @Inject constructor(
|
|||
stopProgressCollection()
|
||||
_sessionStatus.value = SessionStatus.FAILED
|
||||
pendingSessionId = null // Clear pending session ID on failure
|
||||
currentSigningPartyId = null // Clear on failure too
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1366,6 +1380,7 @@ class TssRepository @Inject constructor(
|
|||
val address = AddressUtils.deriveEvmAddress(publicKeyBytes)
|
||||
|
||||
// Save share record
|
||||
// CRITICAL: Save partyId - this is required for signing after backup/restore
|
||||
val shareEntity = ShareRecordEntity(
|
||||
sessionId = apiJoinData.sessionId,
|
||||
publicKey = result.publicKey,
|
||||
|
|
@ -1373,6 +1388,7 @@ class TssRepository @Inject constructor(
|
|||
thresholdT = apiJoinData.thresholdT,
|
||||
thresholdN = apiJoinData.thresholdN,
|
||||
partyIndex = myPartyIndex,
|
||||
partyId = partyId,
|
||||
address = address
|
||||
)
|
||||
val id = shareRecordDao.insertShare(shareEntity)
|
||||
|
|
@ -1516,12 +1532,15 @@ class TssRepository @Inject constructor(
|
|||
_sessionStatus.value = SessionStatus.WAITING
|
||||
|
||||
// Add self to participants
|
||||
val allParticipants = sessionData.participants + Participant(partyId, myPartyIndex)
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
|
||||
val signingPartyId = shareEntity.partyId
|
||||
currentSigningPartyId = signingPartyId // Save for later use in this flow
|
||||
val allParticipants = sessionData.participants + Participant(signingPartyId, myPartyIndex)
|
||||
|
||||
// Start TSS sign
|
||||
val startResult = tssNativeBridge.startSign(
|
||||
sessionId = apiJoinData.sessionId,
|
||||
partyId = partyId,
|
||||
partyId = signingPartyId,
|
||||
partyIndex = myPartyIndex,
|
||||
thresholdT = apiJoinData.thresholdT,
|
||||
thresholdN = shareEntity.thresholdN, // Use original N from keygen
|
||||
|
|
@ -1540,8 +1559,8 @@ class TssRepository @Inject constructor(
|
|||
// Start message routing
|
||||
startMessageRouting(apiJoinData.sessionId, myPartyIndex)
|
||||
|
||||
// Mark ready
|
||||
grpcClient.markPartyReady(apiJoinData.sessionId, partyId)
|
||||
// Mark ready - use signingPartyId (original partyId from keygen)
|
||||
grpcClient.markPartyReady(apiJoinData.sessionId, signingPartyId)
|
||||
|
||||
// Wait for sign result
|
||||
val signResult = tssNativeBridge.waitForSignResult()
|
||||
|
|
@ -1552,18 +1571,20 @@ class TssRepository @Inject constructor(
|
|||
|
||||
val result = signResult.getOrThrow()
|
||||
|
||||
// Report completion
|
||||
// Report completion - use signingPartyId (original partyId from keygen)
|
||||
val signatureBytes = Base64.decode(result.signature, Base64.NO_WRAP)
|
||||
grpcClient.reportCompletion(apiJoinData.sessionId, partyId, signature = signatureBytes)
|
||||
grpcClient.reportCompletion(apiJoinData.sessionId, signingPartyId, signature = signatureBytes)
|
||||
|
||||
_sessionStatus.value = SessionStatus.COMPLETED
|
||||
messageCollectionJob?.cancel()
|
||||
currentSigningPartyId = null // Clear after signing completes
|
||||
|
||||
Result.success(result)
|
||||
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("TssRepository", "Join sign session failed", e)
|
||||
_sessionStatus.value = SessionStatus.FAILED
|
||||
currentSigningPartyId = null // Clear on failure too
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
|
@ -1785,6 +1806,7 @@ class TssRepository @Inject constructor(
|
|||
val address = AddressUtils.deriveEvmAddress(publicKeyBytes)
|
||||
|
||||
// Save share record (use actual thresholds from backend)
|
||||
// CRITICAL: Save partyId - this is required for signing after backup/restore
|
||||
val shareEntity = ShareRecordEntity(
|
||||
sessionId = sessionId,
|
||||
publicKey = result.publicKey,
|
||||
|
|
@ -1792,6 +1814,7 @@ class TssRepository @Inject constructor(
|
|||
thresholdT = actualThresholdT,
|
||||
thresholdN = actualThresholdN,
|
||||
partyIndex = myPartyIndex,
|
||||
partyId = partyId,
|
||||
address = address
|
||||
)
|
||||
val id = shareRecordDao.insertShare(shareEntity)
|
||||
|
|
@ -1900,6 +1923,7 @@ class TssRepository @Inject constructor(
|
|||
}
|
||||
|
||||
// Convert to entity and save
|
||||
// CRITICAL: Preserve the original partyId from backup - this is required for signing
|
||||
val shareRecord = backup.toShareRecord()
|
||||
val entity = ShareRecordEntity(
|
||||
sessionId = shareRecord.sessionId,
|
||||
|
|
@ -1908,6 +1932,7 @@ class TssRepository @Inject constructor(
|
|||
thresholdT = shareRecord.thresholdT,
|
||||
thresholdN = shareRecord.thresholdN,
|
||||
partyIndex = shareRecord.partyIndex,
|
||||
partyId = shareRecord.partyId,
|
||||
address = shareRecord.address,
|
||||
createdAt = shareRecord.createdAt
|
||||
)
|
||||
|
|
@ -1915,7 +1940,7 @@ class TssRepository @Inject constructor(
|
|||
val newId = shareRecordDao.insertShare(entity)
|
||||
val savedShare = shareRecord.copy(id = newId)
|
||||
|
||||
android.util.Log.d("TssRepository", "Imported share backup for address: ${backup.address}")
|
||||
android.util.Log.d("TssRepository", "Imported share backup for address: ${backup.address}, partyId: ${backup.partyId}")
|
||||
Result.success(savedShare)
|
||||
} catch (e: com.google.gson.JsonSyntaxException) {
|
||||
android.util.Log.e("TssRepository", "Invalid JSON format in backup", e)
|
||||
|
|
@ -2312,8 +2337,12 @@ class TssRepository @Inject constructor(
|
|||
val shareEntity = shareRecordDao.getShareById(shareId)
|
||||
?: return@withContext Result.failure(Exception("Share not found"))
|
||||
|
||||
// CRITICAL: Use shareEntity.partyId (original partyId from keygen) for signing
|
||||
// This is required for backup/restore to work - the partyId must match what was used during keygen
|
||||
val signingPartyId = shareEntity.partyId
|
||||
currentSigningPartyId = signingPartyId // Save for waitForSignature
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: participants=${session.participants.size}")
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: sessionId=$sessionId, partyId=$partyId, partyIndex=${shareEntity.partyIndex}")
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: sessionId=$sessionId, signingPartyId=$signingPartyId, partyIndex=${shareEntity.partyIndex}")
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: thresholdT=${session.thresholdT}, thresholdN=${shareEntity.thresholdN}")
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] startSigning: messageHash=${session.messageHash?.take(20)}...")
|
||||
session.participants.forEachIndexed { idx, p ->
|
||||
|
|
@ -2328,10 +2357,10 @@ class TssRepository @Inject constructor(
|
|||
} else {
|
||||
rawMessageHash
|
||||
}
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Calling tssNativeBridge.startSign with cleanMessageHash=${cleanMessageHash.take(20)}...")
|
||||
android.util.Log.d("TssRepository", "[CO-SIGN] Calling tssNativeBridge.startSign with cleanMessageHash=${cleanMessageHash.take(20)}..., signingPartyId=$signingPartyId")
|
||||
val startResult = tssNativeBridge.startSign(
|
||||
sessionId = sessionId,
|
||||
partyId = partyId,
|
||||
partyId = signingPartyId,
|
||||
partyIndex = shareEntity.partyIndex,
|
||||
thresholdT = session.thresholdT,
|
||||
thresholdN = shareEntity.thresholdN,
|
||||
|
|
@ -2359,8 +2388,8 @@ class TssRepository @Inject constructor(
|
|||
startMessageRouting(sessionId, shareEntity.partyIndex)
|
||||
}
|
||||
|
||||
// Mark ready
|
||||
grpcClient.markPartyReady(sessionId, partyId)
|
||||
// Mark ready - use signingPartyId (original partyId from keygen)
|
||||
grpcClient.markPartyReady(sessionId, signingPartyId)
|
||||
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -2386,16 +2415,18 @@ class TssRepository @Inject constructor(
|
|||
|
||||
val result = signResult.getOrThrow()
|
||||
|
||||
// Report completion
|
||||
// Report completion - use currentSigningPartyId (original partyId from keygen)
|
||||
val signatureBytes = Base64.decode(result.signature, Base64.NO_WRAP)
|
||||
val session = _currentSession.value
|
||||
val signingPartyId = currentSigningPartyId ?: partyId
|
||||
if (session != null) {
|
||||
grpcClient.reportCompletion(session.sessionId, partyId, signature = signatureBytes)
|
||||
grpcClient.reportCompletion(session.sessionId, signingPartyId, signature = signatureBytes)
|
||||
}
|
||||
|
||||
stopProgressCollection()
|
||||
_sessionStatus.value = SessionStatus.COMPLETED
|
||||
messageCollectionJob?.cancel()
|
||||
currentSigningPartyId = null // Clear after signing completes
|
||||
|
||||
Result.success(result)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -2759,6 +2790,7 @@ private fun ShareRecordEntity.toShareRecord() = ShareRecord(
|
|||
thresholdT = thresholdT,
|
||||
thresholdN = thresholdN,
|
||||
partyIndex = partyIndex,
|
||||
partyId = partyId,
|
||||
address = address,
|
||||
createdAt = createdAt
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ object AppModule {
|
|||
}
|
||||
}
|
||||
|
||||
// Migration from version 2 to 3: add party_id column to share_records
|
||||
// This is critical for backup/restore - the partyId must be preserved for signing to work
|
||||
private val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
// Add party_id column with empty default (existing records will need to be re-exported)
|
||||
database.execSQL(
|
||||
"ALTER TABLE `share_records` ADD COLUMN `party_id` TEXT NOT NULL DEFAULT ''"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGson(): Gson {
|
||||
|
|
@ -48,7 +59,7 @@ object AppModule {
|
|||
TssDatabase::class.java,
|
||||
"tss_party.db"
|
||||
)
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ data class ShareRecord(
|
|||
val thresholdT: Int,
|
||||
val thresholdN: Int,
|
||||
val partyIndex: Int,
|
||||
val partyId: String, // The original partyId used during keygen - required for signing
|
||||
val address: String,
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
|
@ -165,7 +166,7 @@ data class WalletBalance(
|
|||
*/
|
||||
data class ShareBackup(
|
||||
@SerializedName("version")
|
||||
val version: Int = 1, // Backup format version for future compatibility
|
||||
val version: Int = 2, // Version 2: added partyId field for proper backup/restore
|
||||
|
||||
@SerializedName("sessionId")
|
||||
val sessionId: String,
|
||||
|
|
@ -185,6 +186,9 @@ data class ShareBackup(
|
|||
@SerializedName("partyIndex")
|
||||
val partyIndex: Int,
|
||||
|
||||
@SerializedName("partyId")
|
||||
val partyId: String, // The original partyId used during keygen - CRITICAL for signing after restore
|
||||
|
||||
@SerializedName("address")
|
||||
val address: String,
|
||||
|
||||
|
|
@ -209,6 +213,7 @@ data class ShareBackup(
|
|||
thresholdT = share.thresholdT,
|
||||
thresholdN = share.thresholdN,
|
||||
partyIndex = share.partyIndex,
|
||||
partyId = share.partyId,
|
||||
address = share.address,
|
||||
createdAt = share.createdAt
|
||||
)
|
||||
|
|
@ -227,6 +232,7 @@ data class ShareBackup(
|
|||
thresholdT = thresholdT,
|
||||
thresholdN = thresholdN,
|
||||
partyIndex = partyIndex,
|
||||
partyId = partyId,
|
||||
address = address,
|
||||
createdAt = createdAt
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue