250 lines
8.2 KiB
Markdown
250 lines
8.2 KiB
Markdown
# 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<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 新增签名会话创建方法
|
||
```kotlin
|
||
// 位置: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.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. 测试编译和功能
|