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

628 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 创建 2-of-3 钱包流程分析与潜在Bug
## 理论流程(应该如何工作)
### 环境2台手机 + 1个 server-party-co-managed
```
手机1 (发起者)
1. 调用 createNewSession(walletName="测试", t=2, n=3)
2. 服务器创建会话,返回 sessionId + inviteCode
3. 手机1 显示邀请码二维码
4. server-party-co-managed 检测到新会话自动加入第1个参与者
5. 手机2 扫描二维码,调用 validateInviteCode + joinKeygenViaGrpc第2个参与者
6. 服务器检测到参与者数量 = thresholdT (2)
7. 服务器广播 "session_started" 事件给所有参与者手机1、手机2、server
8. 所有参与者收到事件,调用 startKeygenAsInitiator/startKeygenAsJoiner
9. TSS keygen 协议运行9轮通信
10. 完成,所有参与者保存各自的分片
```
---
## 实际代码流程(当前实现)
### 手机1发起者
#### 步骤 1: 创建会话
**代码位置**: `MainViewModel.kt:253-330`
```kotlin
fun createNewSession(walletName: String, thresholdT: Int, thresholdN: Int, participantName: String) {
safeLaunch { // ← 【潜在问题1】如果抛出异常会被捕获
_uiState.update { it.copy(isLoading = true, error = null) }
val result = repository.createSession(walletName, thresholdT, thresholdN)
result.fold(
onSuccess = { sessionResult ->
_currentSessionId.value = sessionResult.sessionId
_createdInviteCode.value = sessionResult.inviteCode
// 【关键】获取会话状态,检查参与者数量
val statusResult = repository.getSessionStatus(sessionResult.sessionId)
statusResult.fold(
onSuccess = { status ->
_sessionParticipants.value = status.participants.map { ... }
// ✅ 正确显示参与者列表
},
onFailure = { e ->
// ⚠️ 失败时只使用自己
_sessionParticipants.value = listOf(participantName)
}
)
},
onFailure = { e ->
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
)
}
}
```
**潜在问题**:
- ✅ Result 处理正确
- ⚠️ 如果 getSessionStatus 失败,参与者列表不准确
- ⚠️ 但这不影响实际的 keygen 启动
---
#### 步骤 2: 等待 session_started 事件
**代码位置**: `MainViewModel.kt:382-406`
```kotlin
repository.setSessionEventCallback { event ->
when (event.eventType) {
"session_started" -> {
val currentSessionId = _currentSessionId.value
if (currentSessionId != null && event.sessionId == currentSessionId) {
android.util.Log.d("MainViewModel", "Session started event for keygen initiator, triggering keygen")
safeLaunch { // ← 【关键问题!】
startKeygenAsInitiator(
sessionId = currentSessionId,
thresholdT = event.thresholdT,
thresholdN = event.thresholdN,
selectedParties = event.selectedParties
)
}
}
}
}
}
```
**这是 Bug 的根源!**
问题分析:
1. `setSessionEventCallback` 是在 **另一个线程**WebSocket 事件线程)中回调的
2. 在回调中使用 `safeLaunch` 启动协程
3. **如果 `startKeygenAsInitiator` 抛出异常**`safeLaunch` 会捕获并更新 `_uiState.error`
4. 但是,**用户可能没有看到错误提示**,因为:
- UI 可能正在显示"等待参与者"界面
- `_uiState.error` 的更新可能被忽略
- 没有明确的错误反馈路径
---
#### 步骤 3: 执行 keygen
**代码位置**: `MainViewModel.kt:537-570`
```kotlin
private suspend fun startKeygenAsInitiator(
sessionId: String,
thresholdT: Int,
thresholdN: Int,
selectedParties: List<String>
) {
android.util.Log.d("MainViewModel", "Starting keygen as initiator: sessionId=$sessionId, t=$thresholdT, n=$thresholdN")
val result = repository.startKeygenAsInitiator(
sessionId = sessionId,
thresholdT = thresholdT,
thresholdN = thresholdN,
password = ""
)
result.fold(
onSuccess = { share ->
_publicKey.value = share.publicKey
_uiState.update {
it.copy(
lastCreatedAddress = share.address,
successMessage = "钱包创建成功!"
)
}
},
onFailure = { e ->
// ⚠️ 错误被记录到 _uiState.error
_uiState.update { it.copy(error = e.message) }
}
)
}
```
**潜在问题**:
- ✅ Result 处理正确
- ⚠️ 但如果函数本身抛出异常(非 Result.failure外层的 `safeLaunch` 会捕获
- ⚠️ 这会导致**双重错误处理**
1. `startKeygenAsInitiator` 更新 `_uiState.error`(如果是 Result.failure
2. `safeLaunch` 也更新 `_uiState.error`(如果是异常)
---
### 手机2加入者
#### 步骤 1: 扫描邀请码
**代码位置**: `MainViewModel.kt:609-641`
```kotlin
fun validateInviteCode(inviteCode: String) {
safeLaunch {
_uiState.update { it.copy(isLoading = true, error = null) }
val result = repository.validateInviteCode(inviteCode)
result.fold(
onSuccess = { validateResult ->
_joinSessionInfo.value = JoinKeygenSessionInfo(...)
_uiState.update { it.copy(isLoading = false) }
},
onFailure = { e ->
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
)
}
}
```
**状态**: ✅ 处理正确
---
#### 步骤 2: 加入会话
**代码位置**: `MainViewModel.kt:648-706`
```kotlin
fun joinKeygen(inviteCode: String, password: String) {
safeLaunch {
_uiState.update { it.copy(isLoading = true, error = null) }
val result = repository.joinKeygenViaGrpc(
inviteCode = pendingInviteCode,
joinToken = pendingJoinToken,
password = password
)
result.fold(
onSuccess = { joinResult ->
// 【关键】保存 joinResult 用于后续 keygen
pendingJoinKeygenInfo = JoinKeygenInfo(
sessionId = joinResult.sessionId,
partyIndex = joinResult.partyIndex,
partyId = joinResult.partyId,
participantIds = joinResult.participantIds
)
// ✅ 等待 session_started 事件
_uiState.update { it.copy(isLoading = false) }
},
onFailure = { e ->
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
)
}
}
```
**状态**: ✅ 处理正确
---
#### 步骤 3: 等待 session_started 事件
**代码位置**: `MainViewModel.kt:408-413`
```kotlin
// Check if this is for keygen joiner (JoinKeygen)
val joinKeygenInfo = pendingJoinKeygenInfo
if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) {
android.util.Log.d("MainViewModel", "Session started event for keygen joiner, triggering keygen")
startKeygenAsJoiner() // ← 【注意】没有用 safeLaunch 包裹!
}
```
**关键发现!**
对比发起者和加入者:
- **发起者**: `safeLaunch { startKeygenAsInitiator(...) }` ← 包了 safeLaunch
- **加入者**: `startKeygenAsJoiner()` ← 没有包 safeLaunch
**这是不一致的!**
---
#### 步骤 4: 执行 keygen加入者
**代码位置**: `MainViewModel.kt:714-764`
```kotlin
private suspend fun startKeygenAsJoiner() {
safeLaunch { // ← 【注意】这里也用了 safeLaunch
val joinInfo = pendingJoinKeygenInfo ?: return
_uiState.update { it.copy(isLoading = true, error = null) }
val result = repository.startKeygenAsJoiner(
sessionId = joinInfo.sessionId,
partyIndex = joinInfo.partyIndex,
participantIds = joinInfo.participantIds,
password = pendingPassword
)
result.fold(
onSuccess = { share ->
_joinKeygenPublicKey.value = share.publicKey
_uiState.update {
it.copy(
isLoading = false,
successMessage = "成功加入钱包!"
)
}
},
onFailure = { e ->
_uiState.update { it.copy(isLoading = false, error = e.message) }
}
)
}
}
```
**问题**:
- `startKeygenAsJoiner` 自己已经用了 `safeLaunch`
- 但在事件回调中调用它时,**没有**再包一层 `safeLaunch`
- 这和发起者的处理方式不同!
**不一致性总结**:
| 角色 | 事件回调中 | 函数自身 | 总包裹层数 |
|-----|-----------|---------|----------|
| 发起者 | `safeLaunch { startKeygenAsInitiator() }` | 无 safeLaunch | 1层 |
| 加入者 | `startKeygenAsJoiner()` | `safeLaunch { ... }` | 1层 |
虽然都是1层但**位置不同**
---
## 🐛 已发现的Bug清单
### Bug 1: 事件回调中的异常处理不一致 ⚠️
**位置**: `MainViewModel.kt:398-413`
**问题**:
- 发起者:事件回调中使用 `safeLaunch` 包裹
- 加入者:事件回调中直接调用(函数内部有 `safeLaunch`
**影响**:
- 如果发起者的 `startKeygenAsInitiator` 在被 `safeLaunch` 调用**之前**抛出异常(例如参数验证),会被捕获
- 但加入者的 `startKeygenAsJoiner` 在事件回调中直接调用,如果函数调用本身抛出异常(不是内部的),不会被捕获
**建议**: 统一处理方式
---
### Bug 2: safeLaunch 双重包裹可能导致静默失败 🚨
**位置**: `MainViewModel.kt:398-405` + `MainViewModel.kt:537-570`
**问题流程**:
```
事件回调
safeLaunch { // ← 第1层异常捕获
startKeygenAsInitiator()
如果抛出异常 X
}
} catch (e: Exception) { // ← 捕获异常 X
_uiState.update { it.copy(error = ...) } // ← 更新错误
}
```
但是:
1. `startKeygenAsInitiator` 内部已经处理了 `Result.failure`
2. 外层 `safeLaunch` 只能捕获**运行时异常**
3. 如果 `repository.startKeygenAsInitiator` 返回 `Result.failure`,不会抛出异常
4. **所以外层 safeLaunch 实际上没什么用**
**更严重的问题**:
如果 `startKeygenAsInitiator` 内部处理了错误(更新了 `_uiState.error`但UI已经切换到其他状态**用户可能看不到错误**
---
### Bug 3: 参与者数量不足时没有明确错误 ⚠️
**场景**:
- 创建 2-of-3 会话
- server-party-co-managed 没有自动加入(配置错误)
- 只有手机1发起者
- **服务器不会广播 session_started 事件**
**当前行为**:
- 手机1 一直显示"等待参与者加入..."
- **没有超时提示**
- **没有明确的错误消息**
**建议**: 添加超时机制和友好提示
---
### Bug 4: getSessionStatus 失败时参与者列表不准确 ⚠️
**位置**: `MainViewModel.kt:302-321`
```kotlin
val statusResult = repository.getSessionStatus(sessionResult.sessionId)
statusResult.fold(
onSuccess = { status ->
_sessionParticipants.value = status.participants.map { ... }
},
onFailure = { e ->
// ⚠️ 失败时只显示自己
_sessionParticipants.value = listOf(participantName)
}
)
```
**问题**:
- 如果 `getSessionStatus` 失败参与者列表显示为1
- 但实际上可能已经有多个参与者(例如 server-party-co-managed
- **这会误导用户**,以为没人加入
---
### Bug 5: 事件回调中的 return 没有处理 ⚠️
**位置**: `MainViewModel.kt:714` (startKeygenAsJoiner)
```kotlin
private suspend fun startKeygenAsJoiner() {
safeLaunch {
val joinInfo = pendingJoinKeygenInfo ?: return // ← 这个 return 只返回 lambda
// ...
}
}
```
**问题**:
- `return` 只会退出 `safeLaunch` 的 lambda
- 不会更新 UI 状态或显示错误
- **用户不知道为什么 keygen 没有启动**
**建议**: 如果 `joinInfo` 为 null应该记录错误并通知用户
---
## 🔍 为什么会创建失败?
### 最可能的原因
#### 原因 1: server-party-co-managed 没有正确加入 🔴
**检查**:
1. server-party-co-managed 是否正在运行?
2. 配置文件中是否启用了自动加入?
3. 服务器日志中是否有加入记录?
**验证命令**:
```bash
# 检查 server-party-co-managed 日志
tail -f /path/to/server-party-co-managed/logs/server.log | grep "join"
```
**预期日志**:
```
[INFO] Detected new session: sessionId=xxx
[INFO] Auto-joining session as backup party
[INFO] Successfully joined session, partyId=backup-party-1
```
如果**没有这些日志**,说明 server-party-co-managed 没有加入!
---
#### 原因 2: session_started 事件没有被触发 🔴
**条件**:
- 服务器只有在 `participants.size >= thresholdT` 时才会广播 `session_started`
- 2-of-3 需要至少 2 个参与者
**检查**:
1. 服务器端参与者列表有多少个?
2. 手机1 的日志中是否有 "Session started event"
**预期日志手机1**:
```
MainViewModel: === MainViewModel received session event ===
MainViewModel: eventType: session_started
MainViewModel: sessionId: xxxxxxxx
MainViewModel: Session started event for keygen initiator, triggering keygen
```
如果**没有这条日志**,说明事件没有触发!
---
#### 原因 3: startKeygenAsInitiator 内部失败但没有显示错误 🔴
**场景**:
1. `session_started` 事件触发了
2. 调用了 `startKeygenAsInitiator`
3.`repository.startKeygenAsInitiator` 返回 `Result.failure`
4. 错误被记录到 `_uiState.error`
5. **但 UI 没有显示错误**(因为还在"等待参与者"界面)
**检查日志**:
```
MainViewModel: Session started event for keygen initiator, triggering keygen
MainViewModel: Starting keygen as initiator: sessionId=xxx, t=2, n=3
TssRepository: Starting keygen as initiator
TssRepository: Error: [具体错误信息] ← 看这里!
```
如果有这条错误日志,说明 keygen 启动失败了!
---
## 🛠️ 调试步骤
### 步骤 1: 检查 server-party-co-managed
```bash
# 1. 检查进程是否运行
ps aux | grep server-party-co-managed
# 2. 检查配置文件
cat /path/to/server-party-co-managed/config.yml | grep -A 10 "auto_join"
# 3. 查看最近日志
tail -f /path/to/server-party-co-managed/logs/server.log
```
### 步骤 2: 抓取手机1发起者日志
```bash
adb logcat -c
adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|session_started"
```
**重点看**:
1. "Creating new session" → 会话创建
2. "Session created successfully" → 会话创建成功
3. "Session status fetched: X participants" → 参与者数量
4. "Session started event" → 事件触发
5. "Starting keygen as initiator" → keygen 启动
### 步骤 3: 抓取手机2加入者日志
```bash
adb logcat -c
adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|session_started"
```
**重点看**:
1. "Validate success: sessionId=" → 邀请码验证成功
2. "Join keygen success: partyIndex=" → 加入成功
3. "Session started event for keygen joiner" → 收到事件
---
## 🚀 推荐修复方案
### 修复 1: 统一事件回调中的异常处理
```kotlin
repository.setSessionEventCallback { event ->
when (event.eventType) {
"session_started" -> {
// 统一使用 safeLaunch 包裹所有启动函数
val currentSessionId = _currentSessionId.value
if (currentSessionId != null && event.sessionId == currentSessionId) {
android.util.Log.d("MainViewModel", "Session started event for keygen initiator")
safeLaunch {
startKeygenAsInitiator(...)
}
}
val joinKeygenInfo = pendingJoinKeygenInfo
if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) {
android.util.Log.d("MainViewModel", "Session started event for keygen joiner")
safeLaunch { // ← 添加 safeLaunch
startKeygenAsJoiner()
}
}
}
}
}
```
### 修复 2: 移除 startKeygenAsJoiner 内部的 safeLaunch
```kotlin
private suspend fun startKeygenAsJoiner() {
// 移除内部的 safeLaunch由调用方负责异常处理
val joinInfo = pendingJoinKeygenInfo
if (joinInfo == null) {
android.util.Log.e("MainViewModel", "startKeygenAsJoiner: joinInfo is null!")
_uiState.update { it.copy(error = "加入信息丢失,请重试") }
return
}
_uiState.update { it.copy(isLoading = true, error = null) }
val result = repository.startKeygenAsJoiner(...)
// ...
}
```
### 修复 3: 添加超时机制
`createNewSession` 后启动超时计时器:
```kotlin
// 5分钟超时
val timeoutJob = viewModelScope.launch {
delay(300_000) // 5 minutes
if (_currentSessionId.value != null && _publicKey.value == null) {
_uiState.update {
it.copy(
error = "等待超时:参与者数量不足或服务器未响应",
isLoading = false
)
}
}
}
```
---
## 📊 总结
### 最可能导致失败的原因(按概率排序)
1. **🔴 server-party-co-managed 没有自动加入** (70%)
- 检查配置和日志
2. **🔴 session_started 事件没有触发** (20%)
- 参与者数量不足
- WebSocket 连接问题
3. **🟡 startKeygenAsInitiator 失败但错误被忽略** (8%)
- 检查手机日志中的异常
4. **🟢 safeLaunch 包裹问题** (2%)
- 理论上不会导致完全失败
- 但可能导致错误信息不清晰
### 立即行动项
1. **检查 server-party-co-managed 状态** ← 最重要!
2. **抓取手机日志,搜索 "session_started"**
3. **搜索日志中的 "Caught exception" 或 "Error:"**
4. **把日志发给我进行详细分析**
---
**请先按照"调试步骤"抓取日志,然后我们可以精确定位问题!**