Skip to content

fix(sync,p2p): 键控主修 —— delegated→root 亲签事件永不落图(N1–N4 + 四方向矩阵) - #125

Merged
Windsander merged 2 commits into
mainfrom
openchamber/fix-conn-identity
Oct 16, 2026
Merged

Windsander merged 2 commits into
mainfrom
openchamber/fix-conn-identity

Conversation

@Windsander

@Windsander Windsander commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

键控主修 P1:delegated 守护 → root 守护「亲签事件永不落图」修复

缺陷(#122 前存,会话级红样本已钉死到行)

表征:daemon×daemon 下,delegated 侧写亲签 membership/记忆 → root 侧永不落图(同一 LAN 必然触发;单连接沙箱不触发,故 verify:daemon:cluster 从未暴露——它只断言 A→B 这个坏不了的方向)。

真实触发链:join 上车把邀请方地址双键写入地址簿(deviceId + derivePeerIdHex(公钥),F-C12-1)+ mDNS 通告恰为派生 hex 形态 → handleLanDiscovery 命中并以 hex 键对同一对端再拨一次;拨号键因此多形态(libp2p id / deviceId / 派生 hex),被拨侧一律派生 hex。

断点(精确到行):

  1. 连接键不对称:src/p2p/transport/Libp2pProvider.ts:644(被拨侧派生 hex)vs :670-685(拨号键原样)、src/p2p/P2PNetwork.ts:548-590/:593-603(LAN 再拨);去重只按本地键 src/p2p/connection/ConnectionManager.ts:134。
  2. 被拨侧同键坍缩:src/p2p/P2PNetwork.ts:740-746、src/sync/syncmgr/SyncManager.ts:999(旧 entry 被替换,旧 PeerFrames 仍读旧信道)。
  3. nudge 静默丢弃:src/sync/syncmgr/SyncManager.ts:475-478(onlinePeers.get(key)!==entry → 无声 return)。
  4. 全局会话队列饥饿(活锁):SyncManager.ts:1507-1511 + :925-945(常驻响应方无限等 hello 不归还队列)→ 第二条连接的会话体永不启动(帧已解密却无人读)。
    ⇒ B 亲签事件永不落图(双向皆断;首连接上「恰好先完成」的 A→B 推送掩盖问题)。

红样本(沙箱 daemon×daemon,EXTRA_DIAL=hex,连跑 6 次恒定;原始输出见 commit 前的 A1 回执):

KEYING_SUMMARY {"...","defectRed":true}   # 修复前:B 亲签事件 inA=0、双向断
KEYING_SUMMARY {"...","defectRed":false,"passed":10}  # 修复后:同一命令 10/10

如实更正(计划 §1.4 记录,历史可溯)

计划原列的两个嫌疑断点均未触发:①角色仲裁(SyncManager.ts:988/:996)——探针显示两侧证书 deviceId 正确、initiate 双侧互补,静默兜底未执行;②信任快路径(:1386)——失败时事件根本未到达校验点(单连接阶段 verifyEventTrust 全 ok)。真实断点在连接/会话路由键控层(上述 1–4),已按红线 1 顺带删除静默兜底。此更正已在检查点①经主控独立复跑认可。

修复(N1–N4;红线语义:无「线协议 / 策略 / 存储 / 事件格式」变更)

表述更正(检查点②口径):src/sync/protocol.ts 有 +11 行——仅为 SyncTransport 新增内部可选字段 connection(getter 暴露承载连接),供 N4 会话队列粒度使用;非线协议变更(帧格式 / 消息集 / 握手/同步语义均不变)。

  • N1 规范对端键:认证成功即把会话 peerId 规范化为 sha256hex(证书设备公钥)(canonicalPeerIdFromCertificate);channels/onlinePeers/pushTimers/getChannel 全走规范键;拨号键只作寻址提示(peerKeyAliases 翻译)。
  • N2 同设备单活连接:新连接认证成功之后才退役旧连接(convergePeerConnection/retireConnection;ConnectionManager.disconnect(peerId, connection) 按对象关闭 + 身份守卫);connection-closed 携带连接对象,信道/会话/登记清理按代际比对;握手失败恢复同键原有已认证会话(第二次认证失败 → 旧连接继续正常服务,对抗不变式)。
  • N3 响亮失败:三类计数 nudgeDroppedStaleEntry / staleEntrySignals / identityFailures + sync-frames-dropped 事件;attachToNode 缺权威身份 = 显式失败 + 告警(删除静默兜底)。
  • N4 每连接会话队列:enqueue 按连接粒度(SyncTransport.connection),空闲守候不再饿死其他连接/对端。

验证

  • 红→绿:EXTRA_DIAL=hex|device npm run verify:keying:repro 修复前确定性红、修复后 10/10 绿(单连接基线两态均绿)。
  • 判别性单测/集成(4 套新增,含变异抽查 2 条):
    • tests/p2p/keying-canonicalization.test.ts(拨号键不得留在会话身份;删规范化行 → 锚点红)
    • tests/sync/drop-counters.test.ts(三类计数逐一断言;屏蔽 nudge 计数 → 锚点红)
    • tests/sync/session-queue-isolation.test.ts(多对端不互相饿死)
    • tests/integration/keying-convergence.test.ts(第二键再拨收敛 + 异主同名证书对抗)
  • 四方向矩阵 × 第二连接(npm run verify:keying:matrix,13 场景全绿):
    b2a / a2b / 委派↔委派 × {单连接, hex 再拨, deviceId 再拨} + 重连竞态(拨号侧/被拨侧重启 + 掉线事件回填自愈,missing=0)+ 冒名第二连接(认证失败旧连接不受影响)+ feat(sync): daemon-layer auto-grant on subscription (killswitch-gated) #122 主网流程(委派侧订阅 notes → 根侧自动签发触发 → 数据到位)+ 混合版本抽查(旧端↔新端,两向各跑:连接/数据面记录见矩阵输出;旧端双拨遗留形态如实记录、连接面不倒退)。
  • 全量回归:1195/1195(144 套件)+ build/lint 零输出 + check:cleanliness 0/0。
  • 身份不变量定稿:src/sync/IDENTITY-INVARIANTS.md(I1–I7 与实现/测试锚点对照)。

回滚与影响面

  • 回滚 = revert 本 PR(纯内部键控,无数据/协议迁移)。
  • 影响面:P2PNetwork.ts / ConnectionManager.ts / AuthenticationHandshake.ts / SyncManager.ts / protocol.ts(仅新增 SyncTransport 内部可选字段 connection + getter——非线协议变更)+ 测试/脚本/文档;策略、存储、事件与握手/同步帧格式零改动。

证据索引

  • 复现/探针:scripts/keying-repro.mjs、scripts/keying-preload.mjs(本 PR 作者自用;矩阵为其超集)
  • 矩阵:scripts/verify-keying-matrix.mjs(npm run verify:keying:matrix;OLD_MCP=<旧构建入口> 启用混合版本抽查)
  • 提案与更正记录:src/sync/KEYING-FIX-PROPOSAL.md;不变量:src/sync/IDENTITY-INVARIANTS.md

- scripts/keying-repro.mjs:沙箱 daemon×daemon 复现/变体工具(B 委派主动拨 A)。
  单连接=绿;第二键再拨(EXTRA_DIAL=hex/device)=确定性红:B 亲签事件零落 A。
- scripts/keying-preload.mjs:NODE_OPTIONS 注入的六层探针(auth/conn/channel/
  frames/queue/session),钉死断点:连接键不对称(拨号键 3 形态 vs 被拨侧恒派生
  hex)+ 被拨侧同键坍缩 + 全局会话队列饥饿 + nudge 静默丢弃。
- src/sync/KEYING-FIX-PROPOSAL.md:A2 提案(根因到行 / N1–N4 方案 / 红线对照 /
  测试与回滚 / 身份不变量草案 v0)。
- package.json:新增 verify:keying:repro。

供检查点①;src/ 产品代码零改动。
缺陷(#122 前存,已钉死到行):join hints 双键 + mDNS 自动再拨使同一对端出现第二条
连接(拨号键 libp2p id/deviceId/派生 hex 多形态 vs 被拨侧恒派生 hex),被拨侧同键坍缩、
旧 entry 的帧读取者滞留;nudge 落旧 entry 静默丢弃 + 常驻响应方会话占住全局队列 →
delegated→root 亲签事件永不落图。单连接场景不受影响(故集群脚本从未暴露)。

N1 规范对端键:握手认证成功即把 session.peerId 规范化为 sha256hex(证书设备公钥)
(canonicalPeerIdFromCertificate);P2PNode.peerKeyAliases/通道/在线登记/getChannel
全走规范键;拨号键只作寻址提示。
N2 单活连接收敛:新连接**认证成功**后才退役旧连接(按连接对象关闭 + 身份守卫的清理);
connection-closed 携带连接对象,信道/会话/登记清理按代际比对;握手失败恢复同键原有
已认证会话(第二次认证失败不影响旧连接——对抗不变式)。
N3 响亮失败:nudgeDroppedStaleEntry/staleEntrySignals/identityFailures 三类计数 +
sync-frames-dropped 事件;attachToNode 缺权威身份=显式失败+告警(删静默兜底)。
N4 会话队列按连接粒度(SyncTransport.connection 暴露承载连接),空闲守候不再饿死
其他连接/对端的会话。

测试:keying-canonicalization / drop-counters / session-queue-isolation /
keying-convergence(含异主同名证书对抗);矩阵 scripts/verify-keying-matrix.mjs
13 场景(四方向 × 再拨两形态 + 重连竞态 + 冒名 + #122 主网流程 + 混合版本抽查)。
不变量定稿 src/sync/IDENTITY-INVARIANTS.md。
线协议/策略/存储/事件格式零改动。
@Windsander

Copy link
Copy Markdown
Owner Author

证据(P1·B2/B3 · 原始输出摘录)

1) 红→绿(A1 红样本同一命令,连跑 6 次恒定)

# 修复前:EXTRA_DIAL=hex
KEYING_SUMMARY {"variant":"...|xhex","total":10,"failed":[CTRL(A→B), L3(B→A 日志), L4(from-B)],"defectRed":true}
# 修复后:同一命令
KEYING_SUMMARY {"variant":"...|xhex","total":10,"passed":10,"failed":[],"controlOk":true,"defectRed":false}
# 单连接基线(两态均绿):defectRed:false, 9/9

2) 四方向矩阵 × 第二连接 × 重连 × 冒名 × #122 × 混合版本(13/13)

KEYING_MATRIX {"total":13,"passed":13,"failed":[],"scenarios":[
 "b2a-single","b2a-redial-hex","b2a-redial-device",
 "a2b-single","a2b-redial-hex",
 "pair-single","pair-redial-hex",
 "reconnect-dialer","reconnect-acceptor",
 "rogue-second-auth-fails","auto-grant-mainnet-flow",
 "mixed-old-delegated","mixed-old-root"]}
# 关键场景原始 detail:
[auto-grant-mainnet-flow] {"subscriptionVisibleOnRoot":true,"autoGrantVisible":true,"notesDataArrived":true}
[reconnect-acceptor] {"redial":{"status":200},"reconnected":true,"丢件观察":{"healed":true},"liveDelivered":true,"missingBEvents":0}
[rogue-second-auth-fails] {"rogueAuthedAsB":1,"D1_B→A":{...true},"D2_A→B":{...true}}   # 冒名未接纳、旧连接继续服务
[mixed-old-delegated] {"oldSide":"delegated","connected":true,"observed":{"rootSeesB":true,"bSeesA":true,"missingBEvents":0},"delegatedOnline":["device-A","device-A"]}  # 旧端双拨遗留登记形态如实记录(数据面不倒退)
[mixed-old-root]      {"oldSide":"root","connected":true,"observed":{"rootSeesB":true,"bSeesA":true,"missingBEvents":0},"delegatedOnline":["device-A"]}

(命令:npm run verify:keying:matrix;混合版本抽查以 OLD_MCP=<bbc2e13 构建入口> 启用)

3) 判别性抽查(变异 → 锚点红 → 还原绿)

# 屏蔽 onPeerNudge 丢弃计数 → tests/sync/drop-counters.test.ts ✕(nudge 丢弃用例)
# 删除握手会话规范化一行 → tests/p2p/keying-canonicalization.test.ts ✕(拨号键不得留在会话身份)

4) 全量 CI 五项(本地同构)

Tests: 1195 passed, 1195 total(144 套件)
Coverage: All files 92.39 | 79.78 | 84.9 | 94.6(gates 通过)
build / lint / check:cleanliness / check:surface-parity / check:docs / check:deps:零输出
verify:fleet:all (Step 1b 汇总):{"total":163,"passed":163,"failed":[],"skipped":[]}

@Windsander

Copy link
Copy Markdown
Owner Author

表述更正(检查点②):此前多处「零改动」的简化表述不够准确——src/sync/protocol.ts 实为 +11 行,内容为 SyncTransport 新增内部可选字段 connection 与对应 getter(N4 会话队列粒度用)。非线协议变更:帧格式、消息集、握手/同步语义均不变。本条为对 PR 描述的补充更正(代码未改,head SHA 不变)。

@Windsander
Windsander merged commit a873ef4 into main Oct 16, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant