docs(android): 添加调试日志指南和2-of-3流程分析
添加两份关键调试文档:
## 1. DEBUG_LOG_GUIDE.md - 日志抓取指南
- 详细的日志抓取命令(adb logcat)
- 关键日志检查点(会话创建、事件触发、keygen启动)
- 日志分析流程图
- 常见陷阱和解决方案
- 完整的日志模板
## 2. 2OF3_FLOW_ANALYSIS.md - 2-of-3钱包创建流程深度分析
### 已发现的5个潜在Bug:
**Bug 1: 事件回调中的异常处理不一致**
- 发起者:在事件回调中用 safeLaunch 包裹
- 加入者:在函数内部用 safeLaunch
- 位置不同可能导致异常处理行为不一致
**Bug 2: safeLaunch 双重包裹可能导致静默失败** 🚨
- startKeygenAsInitiator 在事件回调中被 safeLaunch 包裹
- 函数内部已经处理了 Result.failure
- 外层 safeLaunch 只能捕获运行时异常
- 如果内部更新了错误但UI已切换状态,用户可能看不到错误
**Bug 3: 参与者数量不足时没有明确错误**
- server-party-co-managed 未加入时,会话不会启动
- 但没有超时提示,用户一直等待
**Bug 4: getSessionStatus 失败时参与者列表不准确**
- 失败时只显示自己
- 实际可能已有多个参与者
- 误导用户
**Bug 5: startKeygenAsJoiner 中的 return 没有错误处理**
- joinInfo 为 null 时静默返回
- 用户不知道为什么 keygen 没启动
### 创建失败的3个最可能原因:
1. **server-party-co-managed 没有正确加入** (70%)
- 需要检查配置和日志
2. **session_started 事件没有被触发** (20%)
- 参与者数量不足
- WebSocket 连接问题
3. **startKeygenAsInitiator 失败但错误被忽略** (8%)
- Result.failure 被处理但UI没显示
### 提供的调试步骤:
1. 检查 server-party-co-managed 状态
2. 抓取手机日志(提供完整命令)
3. 搜索关键日志片段
### 推荐修复方案:
1. 统一事件回调异常处理
2. 移除双重 safeLaunch
3. 添加超时机制
下一步:用户需要抓取日志进行精确诊断
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f3a5b021e
commit
05c6ab3dc4
|
|
@ -0,0 +1,627 @@
|
|||
# 创建 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. **把日志发给我进行详细分析**
|
||||
|
||||
---
|
||||
|
||||
**请先按照"调试步骤"抓取日志,然后我们可以精确定位问题!**
|
||||
|
|
@ -0,0 +1,398 @@
|
|||
# Android 应用调试日志抓取指南
|
||||
|
||||
## 当前日志配置分析
|
||||
|
||||
### ✅ 已有日志点
|
||||
|
||||
应用在关键位置使用 `android.util.Log` 记录日志:
|
||||
|
||||
| 日志标签 | 位置 | 日志内容 |
|
||||
|---------|------|---------|
|
||||
| `MainViewModel` | 所有 ViewModel 操作 | 会话创建、参与者加入、TSS进度、异常 |
|
||||
| `TssRepository` | 所有 Repository 操作 | gRPC调用、TSS native调用、数据库操作 |
|
||||
| `GrpcClient` | 网络通信 | gRPC连接、请求/响应、错误 |
|
||||
| `TssNativeBridge` | TSS原生库 | 密钥生成、签名、错误 |
|
||||
|
||||
### 📋 关键日志内容
|
||||
|
||||
#### 1. 会话创建(创建2-of-3钱包)
|
||||
```
|
||||
MainViewModel: Creating new session: walletName=xxx, t=2, n=3
|
||||
MainViewModel: Session created: sessionId=xxx, inviteCode=xxx
|
||||
MainViewModel: Session status fetched: X participants already joined
|
||||
MainViewModel: Participants: [partyId1, partyId2, ...]
|
||||
```
|
||||
|
||||
#### 2. 密钥生成触发
|
||||
```
|
||||
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
|
||||
TssNativeBridge: keygenAsInitiator called
|
||||
```
|
||||
|
||||
#### 3. 异常捕获(safeLaunch)
|
||||
```
|
||||
MainViewModel: Caught exception in safeLaunch
|
||||
[Stack trace...]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 日志抓取命令
|
||||
|
||||
### 方案 1: 实时日志(推荐用于复现问题)
|
||||
|
||||
```bash
|
||||
# 清除旧日志
|
||||
adb logcat -c
|
||||
|
||||
# 实时查看所有应用日志(带时间戳)
|
||||
adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|TssNativeBridge|AndroidRuntime"
|
||||
```
|
||||
|
||||
### 方案 2: 过滤关键标签(快速定位)
|
||||
|
||||
```bash
|
||||
# 只看应用相关日志
|
||||
adb logcat MainViewModel:D TssRepository:D GrpcClient:D TssNativeBridge:D AndroidRuntime:E *:S
|
||||
```
|
||||
|
||||
### 方案 3: 保存完整日志到文件
|
||||
|
||||
```bash
|
||||
# 清除旧日志
|
||||
adb logcat -c
|
||||
|
||||
# 重现问题(创建2-of-3钱包)
|
||||
|
||||
# 保存日志到文件
|
||||
adb logcat -d -v time > android_debug.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 重点关注的日志
|
||||
|
||||
### 创建 2-of-3 钱包失败时需要看到:
|
||||
|
||||
#### ✅ 必须有的日志(正常流程)
|
||||
1. **会话创建请求**:
|
||||
```
|
||||
MainViewModel: Creating new session: walletName=测试钱包, t=2, n=3, participantName=xxx
|
||||
```
|
||||
|
||||
2. **会话创建成功**:
|
||||
```
|
||||
MainViewModel: Session created successfully
|
||||
MainViewModel: sessionId: xxxxxxxx
|
||||
MainViewModel: inviteCode: ABCD1234
|
||||
```
|
||||
|
||||
3. **获取会话状态**:
|
||||
```
|
||||
MainViewModel: Session status fetched: 2 participants already joined
|
||||
MainViewModel: Participants: [party-id-1, party-id-2]
|
||||
```
|
||||
|
||||
4. **收到 session_started 事件**:
|
||||
```
|
||||
MainViewModel: === MainViewModel received session event ===
|
||||
MainViewModel: eventType: session_started
|
||||
MainViewModel: sessionId: xxxxxxxx
|
||||
```
|
||||
|
||||
5. **触发密钥生成**:
|
||||
```
|
||||
MainViewModel: Session started event for keygen initiator, triggering keygen
|
||||
MainViewModel: Starting keygen as initiator: sessionId=xxx, t=2, n=3
|
||||
```
|
||||
|
||||
6. **TSS 原生库调用**:
|
||||
```
|
||||
TssNativeBridge: keygenAsInitiator called with sessionId=xxx
|
||||
TssNativeBridge: Keygen completed successfully
|
||||
```
|
||||
|
||||
7. **进度更新**:
|
||||
```
|
||||
MainViewModel: Progress update: 1 / 9
|
||||
MainViewModel: Progress update: 2 / 9
|
||||
...
|
||||
MainViewModel: Progress update: 9 / 9
|
||||
```
|
||||
|
||||
#### ❌ 可能出现的错误日志
|
||||
|
||||
1. **safeLaunch 捕获的异常**:
|
||||
```
|
||||
MainViewModel: Caught exception in safeLaunch
|
||||
java.net.SocketTimeoutException: timeout
|
||||
at ...
|
||||
```
|
||||
|
||||
2. **gRPC 连接失败**:
|
||||
```
|
||||
GrpcClient: Failed to connect to server
|
||||
GrpcClient: Error: UNAVAILABLE: io exception
|
||||
```
|
||||
|
||||
3. **TSS 原生库错误**:
|
||||
```
|
||||
TssNativeBridge: keygenAsInitiator failed: [error message]
|
||||
```
|
||||
|
||||
4. **会话创建失败**:
|
||||
```
|
||||
MainViewModel: Service check failed
|
||||
TssRepository: Failed to create session: [error message]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 关键问题检查点
|
||||
|
||||
### 1. 检查 safeLaunch 是否吞掉了异常
|
||||
|
||||
搜索日志中的:
|
||||
```
|
||||
"Caught exception in safeLaunch"
|
||||
```
|
||||
|
||||
如果有这行,说明异常被捕获了,查看后续的堆栈跟踪。
|
||||
|
||||
### 2. 检查是否有 Result.failure 未处理
|
||||
|
||||
搜索:
|
||||
```
|
||||
"onFailure"
|
||||
"Failed to"
|
||||
"Error:"
|
||||
```
|
||||
|
||||
### 3. 检查 session_started 事件是否触发
|
||||
|
||||
搜索:
|
||||
```
|
||||
"Session started event for keygen initiator"
|
||||
```
|
||||
|
||||
如果**没有这行**,说明事件回调没有触发,密钥生成没有启动。
|
||||
|
||||
### 4. 检查参与者计数
|
||||
|
||||
搜索:
|
||||
```
|
||||
"Session status fetched: X participants"
|
||||
```
|
||||
|
||||
如果参与者数量 < thresholdT(例如 2-of-3 需要至少2个参与者),会话不会启动。
|
||||
|
||||
---
|
||||
|
||||
## 📊 日志分析流程图
|
||||
|
||||
```
|
||||
启动应用
|
||||
↓
|
||||
[搜索] "Service check"
|
||||
├─ 成功 → 继续
|
||||
└─ 失败 → 检查数据库/网络/原生库错误
|
||||
↓
|
||||
点击"创建钱包"
|
||||
↓
|
||||
[搜索] "Creating new session"
|
||||
├─ 有 → 继续
|
||||
└─ 无 → UI事件未触发(前端问题)
|
||||
↓
|
||||
[搜索] "Session created successfully"
|
||||
├─ 有 → 继续
|
||||
└─ 无 → 检查 gRPC 错误或 "onFailure"
|
||||
↓
|
||||
[搜索] "Session status fetched: X participants"
|
||||
├─ 有 → 检查参与者数量是否 >= thresholdT
|
||||
└─ 无 → getSessionStatus 调用失败
|
||||
↓
|
||||
[搜索] "Session started event"
|
||||
├─ 有 → 继续
|
||||
└─ 无 → WebSocket 事件未收到(服务器问题)
|
||||
↓
|
||||
[搜索] "Starting keygen as initiator"
|
||||
├─ 有 → 继续
|
||||
└─ 无 → safeLaunch 内部异常(搜索 "Caught exception")
|
||||
↓
|
||||
[搜索] "keygenAsInitiator called"
|
||||
├─ 有 → 检查 "Keygen completed" 或 TSS错误
|
||||
└─ 无 → 原生库调用未执行
|
||||
↓
|
||||
[搜索] "Progress update"
|
||||
├─ 有 → TSS 正在进行,检查是否完成
|
||||
└─ 无 → TSS 未启动或卡住
|
||||
↓
|
||||
[搜索] "Keygen completed successfully"
|
||||
├─ 有 → 成功!
|
||||
└─ 无 → 检查 TSS 错误日志
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 调试建议
|
||||
|
||||
### 如果看到 "Caught exception in safeLaunch"
|
||||
|
||||
**原因**: safeLaunch 捕获了异常但可能没有正确显示给用户
|
||||
|
||||
**临时解决方案**: 查看异常堆栈,找到根本原因
|
||||
|
||||
**示例**:
|
||||
```
|
||||
MainViewModel: Caught exception in safeLaunch
|
||||
java.net.SocketTimeoutException: timeout
|
||||
at okhttp3.internal.connection.RealCall.execute
|
||||
at ...
|
||||
```
|
||||
→ 说明 gRPC 连接超时
|
||||
|
||||
### 如果没有 "Session started event"
|
||||
|
||||
**原因**: WebSocket 事件回调未触发
|
||||
|
||||
**可能问题**:
|
||||
1. service-party 服务器未运行
|
||||
2. WebSocket 连接断开
|
||||
3. 服务器未广播 session_started 事件
|
||||
4. 参与者数量不足(< thresholdT)
|
||||
|
||||
**检查**:
|
||||
```bash
|
||||
# 检查服务器日志
|
||||
tail -f /path/to/service-party/logs/server.log
|
||||
```
|
||||
|
||||
### 如果有 "Session started event" 但没有 "Starting keygen"
|
||||
|
||||
**原因**: safeLaunch 内部的 startKeygenAsInitiator 调用失败
|
||||
|
||||
**检查**:
|
||||
- 搜索 "Caught exception in safeLaunch"
|
||||
- 查看异常类型和堆栈
|
||||
|
||||
---
|
||||
|
||||
## 📝 日志模板(复制给我)
|
||||
|
||||
抓取日志后,请提供以下信息:
|
||||
|
||||
```
|
||||
### 1. 操作步骤
|
||||
- [ ] 启动应用
|
||||
- [ ] 点击"创建钱包"
|
||||
- [ ] 输入钱包名称: ___
|
||||
- [ ] 选择 2-of-3
|
||||
- [ ] 输入参与者名称: ___
|
||||
- [ ] 点击"创建"
|
||||
- [ ] 【描述具体现象】: ___
|
||||
|
||||
### 2. 关键日志片段
|
||||
|
||||
#### 会话创建
|
||||
```
|
||||
[粘贴包含 "Creating new session" 的日志]
|
||||
```
|
||||
|
||||
#### 会话状态
|
||||
```
|
||||
[粘贴包含 "Session status fetched" 的日志]
|
||||
```
|
||||
|
||||
#### 事件触发
|
||||
```
|
||||
[粘贴包含 "Session started event" 的日志]
|
||||
```
|
||||
|
||||
#### 异常(如果有)
|
||||
```
|
||||
[粘贴包含 "Caught exception" 的日志]
|
||||
```
|
||||
|
||||
### 3. 完整日志文件
|
||||
[附件: android_debug.log]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 临时调试增强(如需更详细日志)
|
||||
|
||||
如果标准日志不够详细,可以临时添加更多日志:
|
||||
|
||||
### 在 MainViewModel.kt 的 safeLaunch 中:
|
||||
|
||||
```kotlin
|
||||
private fun safeLaunch(
|
||||
onError: ((Exception) -> Unit)? = null,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) = viewModelScope.launch {
|
||||
try {
|
||||
android.util.Log.d("MainViewModel", "safeLaunch: Starting block") // 添加这行
|
||||
block()
|
||||
android.util.Log.d("MainViewModel", "safeLaunch: Block completed successfully") // 添加这行
|
||||
} catch (e: CancellationException) {
|
||||
android.util.Log.d("MainViewModel", "safeLaunch: CancellationException caught") // 添加这行
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainViewModel", "safeLaunch: Caught exception: ${e.javaClass.simpleName}", e)
|
||||
// ... 现有代码 ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 常见陷阱
|
||||
|
||||
### 1. 异常被吞掉但 UI 不显示错误
|
||||
|
||||
**症状**: 操作无反应,没有错误提示
|
||||
|
||||
**原因**: safeLaunch 更新了 _uiState.error 但 UI 没有订阅
|
||||
|
||||
**检查**: 搜索日志中的 "Caught exception",看是否有异常但 UI 没反应
|
||||
|
||||
### 2. Result.failure 未正确处理
|
||||
|
||||
**症状**: repository 返回 failure 但 ViewModel 没有处理
|
||||
|
||||
**检查**: 搜索 "onFailure" 和 "result.fold"
|
||||
|
||||
### 3. 协程被取消
|
||||
|
||||
**症状**: 操作执行到一半停止
|
||||
|
||||
**检查**: 搜索 "CancellationException"
|
||||
|
||||
---
|
||||
|
||||
## 📱 完整抓取命令(复制粘贴)
|
||||
|
||||
```bash
|
||||
# 1. 清除旧日志
|
||||
adb logcat -c
|
||||
|
||||
# 2. 开始记录(在另一个终端)
|
||||
adb logcat -v time > ~/Desktop/android_debug_$(date +%Y%m%d_%H%M%S).log
|
||||
|
||||
# 3. 操作应用(重现问题)
|
||||
|
||||
# 4. 停止记录(Ctrl+C)
|
||||
|
||||
# 5. 发送日志文件给我
|
||||
```
|
||||
|
||||
或者一步到位(操作完后手动停止):
|
||||
```bash
|
||||
adb logcat -c && adb logcat -v time | tee android_debug.log | grep --color -E "MainViewModel|TssRepository|GrpcClient|TssNativeBridge|Exception|Error"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**准备好后,请执行上述命令,重现创建2-of-3失败的问题,然后把日志发给我!**
|
||||
Loading…
Reference in New Issue