rwadurian/backend/mpc-system/services/service-party-android/IMPLEMENTATION_PLAN.md

8.2 KiB
Raw Blame History

2-of-3 服务器参与选项 - 纯新增实施方案

目标

允许 2-of-3 MPC 用户勾选"包含服务器备份"参与签名,以便在丢失一个设备时转出资产。

核心设计

安全限制

  • 2-of-3 配置显示此选项
  • 其他配置3-of-5, 4-of-7等不显示

实施范围

  • 只修改 Android 客户端
  • 不需要修改后端account-service, message-router
  • 纯新增代码,现有逻辑保持不变

修改文件清单

1. TssRepository.kt2处新增

1.1 新增辅助方法private

// 位置3712行之前类内部末尾
/**
 * 构建参与方列表(新增辅助方法)
 * @param participants 所有参与方
 * @param includeServerParties 是否包含服务器方(默认 false保持现有行为
 */
private fun buildSigningParticipantList(
    participants: List<ParticipantStatusInfo>,
    includeServerParties: Boolean = false
): List<Pair<String, Int>> {
    val filtered = if (includeServerParties) {
        // 包含所有参与方(含服务器)
        participants
    } else {
        // 过滤掉服务器方(现有行为)
        participants.filter { !it.partyId.startsWith("co-managed-party-") }
    }
    return filtered.map { Pair(it.partyId, it.partyIndex) }
}

1.2 新增签名会话创建方法

// 位置buildSigningParticipantList 之后
/**
 * 创建签名会话(支持选择是否包含服务器)
 * @param includeServerBackup 是否包含服务器备份参与方(仅 2-of-3 时使用)
 * 新增方法,不影响现有 createSignSession
 */
suspend fun createSignSessionWithOptions(
    shareId: Long,
    messageHash: String,
    password: String,
    initiatorName: String,
    includeServerBackup: Boolean = false  // 新增参数
): Result<SignSessionResult> {
    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.kt1处新增

// 位置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.ktUI 新增)

// 在交易确认界面新增复选框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传递参数

// 修改 TransferScreen 的 onConfirmTransaction 回调
onConfirmTransaction = { includeServer ->
    viewModel.initiateSignSessionWithOptions(
        shareId = shareId,
        password = "",
        includeServerBackup = includeServer
    )
}

测试场景

场景12-of-3 正常使用(不勾选)

  • 设备A + 设备B 签名
  • 服务器被过滤(现有行为)

场景22-of-3 设备丢失(勾选)

  • 设备A + 服务器 签名
  • 用户明确勾选"包含服务器备份"

场景33-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. 测试编译和功能