From bb6febb46b63a3141aa28b4f935f5c52aaf5df73 Mon Sep 17 00:00:00 2001 From: hailin Date: Mon, 26 Jan 2026 21:42:36 -0800 Subject: [PATCH] =?UTF-8?q?fix(android):=20=E4=BF=AE=E5=A4=8D=E5=8F=82?= =?UTF-8?q?=E4=B8=8E=E8=80=85=E8=AE=A1=E6=95=B0=E7=AB=9E=E6=80=81=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6,=E4=BD=BF=E7=94=A8=E6=9C=8D=E5=8A=A1=E7=AB=AF?= =?UTF-8?q?=E6=9D=83=E5=A8=81=E6=95=B0=E6=8D=AE=20[P1-1]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【架构安全修复 - 防止参与者计数竞态条件】 ## 问题背景 原代码在处理 party_joined 事件时使用本地计数器递增来生成参与者名称。 ## 存在的风险 1. 事件重放导致重复添加 - 网络重连后事件可能重新发送 2. 事件乱序导致编号错乱 - 网络延迟导致事件乱序到达 3. 状态不一致 - 本地状态与服务端真实状态不同步 4. 并发事件处理 - 多个事件快速连续到达时可能冲突 ## 修复方案 使用服务端权威数据构建参与者列表: - BEFORE: 本地计数器累加 (不可靠) - AFTER: 使用 event.selectedParties.size 重建列表 (可靠) 核心改变: val participantCount = event.selectedParties.size // 来自服务端 val participantList = List(participantCount) { index -> "参与方 ${index + 1}" } _sessionParticipants.value = participantList // 直接设置,不累加 ## 为什么这样修复有效 1. 单一真实来源: event.selectedParties 来自服务端 2. 幂等性: 无论事件重放多少次结果相同 3. 顺序无关: 无论事件以何种顺序到达最终状态正确 4. 自动去重: 直接设置 value 而非累加 ## 影响范围 修改 MainViewModel.kt 的 party_joined 事件处理逻辑,影响 4 个参与者列表: 1. _sessionParticipants - 创建钱包页面 2. _joinKeygenParticipants - 加入密钥生成页面 3. _coSignParticipants - 联合签名加入页面 4. _signParticipants - 转账签名发起页面 行为变化: - BEFORE: 每个事件递增计数器累加参与者 - AFTER: 每个事件重建整个参与者列表基于服务端数据 Co-Authored-By: Claude Sonnet 4.5 --- .../presentation/viewmodel/MainViewModel.kt | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt index cdd7fbf3..c1abe192 100644 --- a/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt +++ b/backend/mpc-system/services/service-party-android/app/src/main/java/com/durian/tssparty/presentation/viewmodel/MainViewModel.kt @@ -373,51 +373,55 @@ class MainViewModel @Inject constructor( } } "party_joined", "participant_joined" -> { - android.util.Log.d("MainViewModel", "Processing participant_joined event...") + /** + * 【架构安全修复 - 防止参与者计数竞态条件】 + * + * 原问题:使用 current.size + 1 递增计数器存在多个风险 + * 1. 事件重放:重连后事件可能重复发送,导致参与者重复添加 + * 2. 事件乱序:网络延迟可能导致事件乱序到达,参与者编号错乱 + * 3. 状态不一致:本地计数与服务端真实参与者列表不同步 + * + * 修复方案:使用事件的 selectedParties 字段构建权威的参与者列表 + * - selectedParties 来自服务端,是参与者的唯一真实来源 + * - 根据 selectedParties.size 构建参与者列表,确保与服务端一致 + * - 防止重复添加和计数错乱 + */ + android.util.Log.d("MainViewModel", "Processing participant_joined event (selectedParties=${event.selectedParties.size})...") + + // Build participant list from authoritative server data + val participantCount = event.selectedParties.size + val participantList = List(participantCount) { index -> "参与方 ${index + 1}" } + android.util.Log.d("MainViewModel", " → Building participant list from server data: $participantList") // Update participant count for initiator's CreateWallet screen val currentSessionId = _currentSessionId.value android.util.Log.d("MainViewModel", " Checking for initiator: currentSessionId=$currentSessionId, eventSessionId=${event.sessionId}") if (currentSessionId != null && event.sessionId == currentSessionId) { - android.util.Log.d("MainViewModel", " → Matched initiator session! Updating _sessionParticipants") - _sessionParticipants.update { current -> - val newParticipant = "参与方 ${current.size + 1}" - android.util.Log.d("MainViewModel", " → Adding participant: $newParticipant, total now: ${current.size + 1}") - current + newParticipant - } + android.util.Log.d("MainViewModel", " → Matched initiator session! Updating _sessionParticipants to: $participantList") + _sessionParticipants.value = participantList } // Update participant count for keygen joiner's JoinKeygen screen val joinKeygenInfo = pendingJoinKeygenInfo android.util.Log.d("MainViewModel", " Checking for joiner: joinKeygenInfo?.sessionId=${joinKeygenInfo?.sessionId}") if (joinKeygenInfo != null && event.sessionId == joinKeygenInfo.sessionId) { - android.util.Log.d("MainViewModel", " → Matched joiner session! Updating _joinKeygenParticipants") - _joinKeygenParticipants.update { current -> - val newParticipant = "参与方 ${current.size + 1}" - current + newParticipant - } + android.util.Log.d("MainViewModel", " → Matched joiner session! Updating _joinKeygenParticipants to: $participantList") + _joinKeygenParticipants.value = participantList } // Update participant count for sign joiner's CoSign screen val joinSignInfo = pendingJoinSignInfo if (joinSignInfo != null && event.sessionId == joinSignInfo.sessionId) { - android.util.Log.d("MainViewModel", " → Matched sign joiner session! Updating _coSignParticipants") - _coSignParticipants.update { current -> - val newParticipant = "参与方 ${current.size + 1}" - current + newParticipant - } + android.util.Log.d("MainViewModel", " → Matched sign joiner session! Updating _coSignParticipants to: $participantList") + _coSignParticipants.value = participantList } // Update participant count for sign initiator's TransferScreen (SigningScreen) val signSessionId = _signSessionId.value android.util.Log.d("MainViewModel", " Checking for sign initiator: signSessionId=$signSessionId, eventSessionId=${event.sessionId}") if (signSessionId != null && event.sessionId == signSessionId) { - android.util.Log.d("MainViewModel", " → Matched sign initiator session! Updating _signParticipants") - _signParticipants.update { current -> - val newParticipant = "参与方 ${current.size + 1}" - android.util.Log.d("MainViewModel", " → Adding participant: $newParticipant, total now: ${current.size + 1}") - current + newParticipant - } + android.util.Log.d("MainViewModel", " → Matched sign initiator session! Updating _signParticipants to: $participantList") + _signParticipants.value = participantList } } "all_joined" -> {