# 2-of-3 服务器参与选项 - 纯新增实施方案 ## 目标 允许 2-of-3 MPC 用户勾选"包含服务器备份"参与签名,以便在丢失一个设备时转出资产。 ## 核心设计 ### 安全限制 - **仅** 2-of-3 配置显示此选项 - 其他配置(3-of-5, 4-of-7等)不显示 ### 实施范围 - ✅ 只修改 Android 客户端 - ❌ **不需要**修改后端(account-service, message-router) - ✅ 纯新增代码,现有逻辑保持不变 ## 修改文件清单 ### 1. TssRepository.kt(2处新增) #### 1.1 新增辅助方法(private) ```kotlin // 位置:3712行之前(类内部末尾) /** * 构建参与方列表(新增辅助方法) * @param participants 所有参与方 * @param includeServerParties 是否包含服务器方(默认 false,保持现有行为) */ private fun buildSigningParticipantList( participants: List, includeServerParties: Boolean = false ): List> { val filtered = if (includeServerParties) { // 包含所有参与方(含服务器) participants } else { // 过滤掉服务器方(现有行为) participants.filter { !it.partyId.startsWith("co-managed-party-") } } return filtered.map { Pair(it.partyId, it.partyIndex) } } ``` #### 1.2 新增签名会话创建方法 ```kotlin // 位置:buildSigningParticipantList 之后 /** * 创建签名会话(支持选择是否包含服务器) * @param includeServerBackup 是否包含服务器备份参与方(仅 2-of-3 时使用) * 新增方法,不影响现有 createSignSession */ suspend fun createSignSessionWithOptions( shareId: Long, messageHash: String, password: String, initiatorName: String, includeServerBackup: Boolean = false // 新增参数 ): Result { return withContext(Dispatchers.IO) { try { val shareEntity = shareRecordDao.getShareById(shareId) ?: return@withContext Result.failure(Exception("Share not found")) val signingPartyIdForEvents = shareEntity.partyId android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Creating sign session with includeServerBackup=$includeServerBackup") ensureSessionEventSubscriptionActive(signingPartyIdForEvents) val keygenStatusResult = getSessionStatus(shareEntity.sessionId) if (keygenStatusResult.isFailure) { return@withContext Result.failure(Exception("无法获取 keygen 会话的参与者信息: ${keygenStatusResult.exceptionOrNull()?.message}")) } val keygenStatus = keygenStatusResult.getOrThrow() // 使用新的辅助方法构建参与方列表 val signingParties = buildSigningParticipantList( keygenStatus.participants, includeServerBackup ) android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] Signing parties: ${signingParties.size} of ${keygenStatus.participants.size} (includeServer=$includeServerBackup)") signingParties.forEach { (id, index) -> android.util.Log.d("TssRepository", "[CO-SIGN-OPTIONS] party_id=${id.take(16)}, party_index=$index") } if (signingParties.size < shareEntity.thresholdT) { return@withContext Result.failure(Exception( "签名参与方不足: 需要 ${shareEntity.thresholdT} 个,但只有 ${signingParties.size} 个参与方" )) } // 后续逻辑与 createSignSession 相同 // ... 构建请求、创建session、加入gRPC等 // (复用现有 createSignSession 的代码) // 调用现有方法的内部逻辑(需要提取) createSignSessionInternal( shareEntity, signingParties, messageHash, password, initiatorName ) } catch (e: Exception) { Result.failure(e) } } } ``` ### 2. MainViewModel.kt(1处新增) ```kotlin // 位置:initiateSignSession 方法之后 /** * 创建签名会话(支持选择服务器参与) * 新增方法,不影响现有 initiateSignSession */ fun initiateSignSessionWithOptions( shareId: Long, password: String, initiatorName: String = "发起者", includeServerBackup: Boolean = false // 新增参数 ) { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, error = null) } val tx = _preparedTx.value if (tx == null) { _uiState.update { it.copy(isLoading = false, error = "交易未准备") } return@launch } android.util.Log.d("MainViewModel", "[SIGN-OPTIONS] Initiating sign session with includeServerBackup=$includeServerBackup") val result = repository.createSignSessionWithOptions( shareId = shareId, messageHash = tx.signHash, password = password, initiatorName = initiatorName, includeServerBackup = includeServerBackup // 传递参数 ) result.fold( onSuccess = { sessionResult -> _signSessionId.value = sessionResult.sessionId _signInviteCode.value = sessionResult.inviteCode _signParticipants.value = listOf(initiatorName) _uiState.update { it.copy(isLoading = false) } pendingSignInitiatorInfo = PendingSignInitiatorInfo( sessionId = sessionResult.sessionId, shareId = shareId, password = password ) if (sessionResult.sessionAlreadyInProgress) { startSigningProcess(sessionResult.sessionId, shareId, password) } }, onFailure = { e -> _uiState.update { it.copy(isLoading = false, error = e.message) } } ) } } ``` ### 3. TransferScreen.kt(UI 新增) ```kotlin // 在交易确认界面新增复选框(Step 2) // 位置:密码输入框之后 // 仅在 2-of-3 时显示 if (wallet.thresholdT == 2 && wallet.thresholdN == 3) { Spacer(modifier = Modifier.height(16.dp)) var includeServerBackup by remember { mutableStateOf(false) } Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = includeServerBackup, onCheckedChange = { includeServerBackup = it } ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = "包含服务器备份参与签名", style = MaterialTheme.typography.bodyMedium ) Text( text = "如果您丢失了一个设备,勾选此项以使用服务器备份完成签名", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } ``` ### 4. MainActivity.kt(传递参数) ```kotlin // 修改 TransferScreen 的 onConfirmTransaction 回调 onConfirmTransaction = { includeServer -> viewModel.initiateSignSessionWithOptions( shareId = shareId, password = "", includeServerBackup = includeServer ) } ``` ## 测试场景 ### 场景1:2-of-3 正常使用(不勾选) - 设备A + 设备B 签名 ✅ - 服务器被过滤(现有行为) ### 场景2:2-of-3 设备丢失(勾选) - 设备A + 服务器 签名 ✅ - 用户明确勾选"包含服务器备份" ### 场景3:3-of-5 配置 - 不显示复选框 ✅ - 保持现有行为 ## 优势 1. ✅ **零后端修改**:后端只接收 parties 数组 2. ✅ **完全向后兼容**:默认行为不变 3. ✅ **安全限制**:仅 2-of-3 可用 4. ✅ **纯新增**:不修改现有方法 5. ✅ **用户明确选择**:需要主动勾选 ## 实施顺序 1. TssRepository:新增辅助方法 2. TssRepository:新增 createSignSessionWithOptions 3. MainViewModel:新增 initiateSignSessionWithOptions 4. TransferScreen:新增 UI 复选框 5. MainActivity:传递参数 6. 测试编译和功能