18 KiB
创建 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
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
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 的根源!
问题分析:
setSessionEventCallback是在 另一个线程(WebSocket 事件线程)中回调的- 在回调中使用
safeLaunch启动协程 - 如果
startKeygenAsInitiator抛出异常,safeLaunch会捕获并更新_uiState.error - 但是,用户可能没有看到错误提示,因为:
- UI 可能正在显示"等待参与者"界面
_uiState.error的更新可能被忽略- 没有明确的错误反馈路径
步骤 3: 执行 keygen
代码位置: MainViewModel.kt:537-570
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会捕获 - ⚠️ 这会导致双重错误处理:
startKeygenAsInitiator更新_uiState.error(如果是 Result.failure)safeLaunch也更新_uiState.error(如果是异常)
手机2(加入者)
步骤 1: 扫描邀请码
代码位置: MainViewModel.kt:609-641
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
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
// 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
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 = ...) } // ← 更新错误
}
但是:
startKeygenAsInitiator内部已经处理了Result.failure- 外层
safeLaunch只能捕获运行时异常 - 如果
repository.startKeygenAsInitiator返回Result.failure,不会抛出异常 - 所以外层 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
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)
private suspend fun startKeygenAsJoiner() {
safeLaunch {
val joinInfo = pendingJoinKeygenInfo ?: return // ← 这个 return 只返回 lambda
// ...
}
}
问题:
return只会退出safeLaunch的 lambda- 不会更新 UI 状态或显示错误
- 用户不知道为什么 keygen 没有启动
建议: 如果 joinInfo 为 null,应该记录错误并通知用户
🔍 为什么会创建失败?
最可能的原因
原因 1: server-party-co-managed 没有正确加入 🔴
检查:
- server-party-co-managed 是否正在运行?
- 配置文件中是否启用了自动加入?
- 服务器日志中是否有加入记录?
验证命令:
# 检查 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 的日志中是否有 "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 内部失败但没有显示错误 🔴
场景:
session_started事件触发了- 调用了
startKeygenAsInitiator - 但
repository.startKeygenAsInitiator返回Result.failure - 错误被记录到
_uiState.error - 但 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
# 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(发起者)日志
adb logcat -c
adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|session_started"
重点看:
- "Creating new session" → 会话创建
- "Session created successfully" → 会话创建成功
- "Session status fetched: X participants" → 参与者数量
- "Session started event" → 事件触发
- "Starting keygen as initiator" → keygen 启动
步骤 3: 抓取手机2(加入者)日志
adb logcat -c
adb logcat -v time | grep -E "MainViewModel|TssRepository|GrpcClient|session_started"
重点看:
- "Validate success: sessionId=" → 邀请码验证成功
- "Join keygen success: partyIndex=" → 加入成功
- "Session started event for keygen joiner" → 收到事件
🚀 推荐修复方案
修复 1: 统一事件回调中的异常处理
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
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 后启动超时计时器:
// 5分钟超时
val timeoutJob = viewModelScope.launch {
delay(300_000) // 5 minutes
if (_currentSessionId.value != null && _publicKey.value == null) {
_uiState.update {
it.copy(
error = "等待超时:参与者数量不足或服务器未响应",
isLoading = false
)
}
}
}
📊 总结
最可能导致失败的原因(按概率排序)
-
🔴 server-party-co-managed 没有自动加入 (70%)
- 检查配置和日志
-
🔴 session_started 事件没有触发 (20%)
- 参与者数量不足
- WebSocket 连接问题
-
🟡 startKeygenAsInitiator 失败但错误被忽略 (8%)
- 检查手机日志中的异常
-
🟢 safeLaunch 包裹问题 (2%)
- 理论上不会导致完全失败
- 但可能导致错误信息不清晰
立即行动项
- 检查 server-party-co-managed 状态 ← 最重要!
- 抓取手机日志,搜索 "session_started"
- 搜索日志中的 "Caught exception" 或 "Error:"
- 把日志发给我进行详细分析
请先按照"调试步骤"抓取日志,然后我们可以精确定位问题!