Skip to content

Commit 387ceb9

Browse files
committed
feat(sra): port the uint64 id identity rework onto b521e61
Port 0f825a7 (feat(sra): replace address+successor chain with uint64 id identity) onto the mainline b521e61, which already carries all PR #24 review fixes (A3 three-state submitShares, B1 mirror-window guard + one-step jump, S1 remove exclusion semantics, time-driven _quarterOf/_syncMirror/ _pendingSharesQuarter). The identity layer is orthogonal to the mirror layer, so the port only rekeys the identity layer and keeps every review fix intact. Orchestrator identity becomes an internal monotonically allocated, never-reused uint64 id (activeIdOf/nextId/admittedIds); an address is only the current effective wallet. replace() is an O(1) wallet re-point that keeps mirror FPV continuity for already-posted quarters; re-admit allocates a fresh id, eliminating the alias-chain residual-state bug class (T10). binding/unclaimed checks read the bound id's admitted directly. - storage: OrchestratorInfo packs 30B into slot0 (wallet/admitted/ frozenAtPostEnd/frozenSince; successor removed); Registry rekeys to id (orchestrators:uint64, activeIdOf, bindings:uint64, nextId, admittedIds) - main contract: admit allocates a fresh id; remove keeps the id record (fpv/prevFpv retained for audit) and clears only the address mapping; replace = O(1) wallet re-point; all read/write paths resolve via activeIdOf; _isAdmitted/_resolve removed, _requireAdmittedId added; _swapRemove(uint64[]) - invariant handler: generation modeling (_idGen/_genSeq, PairRecord.gen) — an address hosts successive identities, so bindings are disambiguated by generation; replace moves only the current generation's pairs; I2/I3c re-aligned (A3 lesson: model must track the implementation) - tests: +5 (re-admit fresh identity, id monotonic never-reused, replace historical-FPV-kept, share-map writes current wallet, correctVolume via new address); existing replace/re-admit tests pass unchanged (same observable behavior) - docs: S13 decision record + sra-design.md id-model sync + impl/001/002 design & test-plan docs Verified: 319 tests (17 suites) + halmos 2/2 + 3-seed invariant; runtime 25,576B exceeds the EIP-170 cap — tracked in docs/sra-design.md §5.12, resolution is the planned contract split (logic-to-library / proxy split, #5).
1 parent b521e61 commit 387ceb9

8 files changed

Lines changed: 787 additions & 168 deletions
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# id 身份模型 × mirror 重构融合实现方案
2+
3+
## 1. 目标和核心功能
4+
5+
### 目标
6+
7+
`sra@b521e61`(已含全部 review 修复:A3 三态 submitShares、B1 守卫+一步跳转、S1 remove 排除语义、时间驱动化 `_quarterOf`/`_syncMirror`/`_pendingSharesQuarter`)基础上,将 SRA 身份模型从"address 即身份 + successor alias 链"改为 **uint64 内部 id 即身份 + activeIdOf 地址重映射****mirror 层(季度数据组织:activeQ/lastSubmittedQ/totalUsd/fpv/prevFpv/frozenSince/frozenAtPostEnd)必须保留不动**,本次只重构身份层。
8+
9+
参考源材料(磁盘上早期实现 实现,基于旧基线):`/tmp/ghost-worktrees/refactor/sra-orch-id/`
10+
11+
### 核心功能
12+
13+
- replace = **O(1) wallet 重映射**(activeIdOf 重映射 + wallet 更新),删除基线"复制 OrchestratorInfo"逻辑;历史季度 FPV 天然跟随 id
14+
- re-admit(remove/replace 后同地址) = **新 id fresh identity**,零清理逻辑,T10 缺陷类从结构消除
15+
- remove 惰性清理 bindings(bindings 按 id 存,认领时查 `orchestrators[id].admitted`
16+
- 删除 `_resolve`/`successor` 全部链逻辑(字段 + 辅助函数 + 3 个调用点:registerPairs/submitShares/bindingOf)
17+
- 外部 ABI(方法签名、事件)不变;mirror 语义不变
18+
19+
## 2. 技术方案
20+
21+
### 2.1 技术栈
22+
23+
Solidity 0.8.36 + Foundry(forge test/fmt/lint)、halmos(symbolic verification,本次**无需改动**)。仓库不变。
24+
25+
### 2.2 技术选型与关键决策
26+
27+
**决策依据(源自 sra-pr24 review 结论 + 本方案核验):**
28+
29+
| 决策点 | 结论 | 理由 |
30+
|---|---|---|
31+
| 身份模型 | **uint64 内部 id**(弃 alias 链) | alias 链的 T10 缺陷类(replace 后 re-admit 残留链致冻结者获份额)是结构性影子——任何实现都必须在 admit 防御;id 方案从结构消除。新基线已通过"replace 复制贡献槽 + admit fresh identity 兜底"实现部分收益,但保留链 = 保留结构性风险 |
32+
| mirror 层 | **完全保留不动** | mirror 是季度数据组织(fpv/prevFpv/totalUsd),id 是身份组织(wallet→id),两者**正交**;id 融合后 replace 还能删掉基线"复制贡献槽"逻辑(数据天然跟随 id),比基线更简化 |
33+
| replace 语义 | O(1) 重映射,历史季度 FPV 保留 | 与基线"复制贡献槽"行为目标一致(S13 决策已记录),实现更简 |
34+
| ABI | 不变 | 方法签名/事件保持 address 语义(FIP 规格),id 是纯内部实现细节 |
35+
36+
**排除方案:** ① 保持 alias 链(基线现状)——T10 结构性风险仍在;② 在新基线上重放早期实现 diff——存储模型已分叉(冻结数组→frozenSince 标志、fpv mapping→实体槽),文本冲突是表象、语义冲突是实质,必须按新存储模型重做而非搬 diff。
37+
38+
### 2.3 存储结构设计(核心决策)
39+
40+
`src/lib/SraStorage.sol` 改造(namespace 常量不变):
41+
42+
```solidity
43+
struct OrchestratorInfo {
44+
address wallet; // 当前有效地址(replace 更新;submitShares 写这个)— 20B
45+
bool admitted; // admitted — 1B
46+
bool frozenAtPostEnd; // mirror 快照标志(保留基线语义)— 1B
47+
Epoch frozenSince; // 当前冻结状态(保留基线语义)— 8B
48+
// 30B 打包进 slot0。与基线同为 3 slots/实体:基线是 admitted/frozenAtPostEnd/frozenSince/successor
49+
// (30B 全部打包 slot0,无 successor 独占槽)+ fpv + prevFpv;id identity 是 successor → wallet 的字段等量替换
50+
// (30B 打包 slot0)+ fpv + prevFpv —— 不省 slot,而是去掉 successor 链字段本身(id 模型无需链)。
51+
FixedU18 fpv; // mirror 活跃季度贡献(保留)— slot1
52+
FixedU18 prevFpv; // mirror 前季度贡献(保留)— slot2
53+
// successor 删除
54+
}
55+
56+
struct SraStorageRegistry {
57+
mapping(uint64 id => OrchestratorInfo) orchestrators; // slot0 — id 是身份(单调递增、不重用)
58+
mapping(address orch => uint64 id) activeIdOf; // slot1 — 0 = 未注册哨兵
59+
mapping(bytes32 pairId => uint64 id) bindings; // slot2 — 存 id(替代 address)
60+
uint64 nextId; // slot3 — id 分配器(构造器置 1)
61+
uint64[] admittedIds; // slot4 — 可枚举 admitted(替代 admittedList address[],length 即 count)
62+
// admittedCount 删除(admittedIds.length 派生,与基线一致)
63+
}
64+
65+
struct SraStorageQuarter { /* 不变:activeQ / lastSubmittedQ / totalUsd */ }
66+
```
67+
68+
**id 单调性测试的 slot 偏移**(测试侧读 `REGISTRY_SLOT + 3` 取 nextId 低 64 位;早期实现 是 `+3` 高 64 位因 admittedCount 打包,新布局 nextId 独占 slot3 低 64 位)。
69+
70+
### 2.4 方法改造清单(逐一列出)
71+
72+
| 方法 | 改造 | mirror 相关逻辑 |
73+
|---|---|---|
74+
| 构造器 | 追加 `_registry().nextId = 1` ||
75+
| `_advanceMirror` | 遍历 `r.admittedIds`(uint64[]),`o = r.orchestrators[r.admittedIds[i]]` | **保留**(prevFpv 快照/fpv 清零/frozenAtPostEnd 复位) |
76+
| `registerPairs` | `id = activeIdOf[msg.sender]``require(id != 0 && o.admitted)`;唯一性改 `boundId != 0 && orchestrators[boundId].admitted`**_resolve**);`bindings[pairId] = id` ||
77+
| `postVolume` | id 解析(`activeIdOf[msg.sender]`| **保留**(advance 触发/fpv 实体槽/AlreadyPosted/MAX_FPV_USD/totalUsd) |
78+
| `admit` | `require(activeIdOf[orch] == 0, AlreadyAdmitted)``require(admittedIds.length < MAX, AtCapacity)``id = nextId++`;新实体 `wallet=orch; admitted=true`(其余零,天然 fresh);`activeIdOf[orch]=id; admittedIds.push(id)` ||
79+
| `remove` | `id = activeIdOf[orch]``require(id != 0 && o.admitted)`**保留归档 id 记录**(wallet/fpv 供审计),`o.admitted=false``activeIdOf[orch]=0``_swapRemove(admittedIds, id)` | **保留**(pending 守卫/镜像扣减 `!frozenAtPostEnd && fpv>0`|
80+
| `freeze`/`unfreeze` | id 解析 | **保留**(frozenSince/frozenAtPostEnd/totalUsd 扣减与恢复) |
81+
| `replace` | **核心改造**`id = activeIdOf[oldOrch]`;require admitted;`require(activeIdOf[newOrch] == 0, AlreadyAdmitted)``activeIdOf[oldOrch]=0; activeIdOf[newOrch]=id; orchestrators[id].wallet=newOrch`**删除基线整块"复制 OrchestratorInfo"逻辑**(fpv/prevFpv/frozenSince/frozenAtPostEnd 跟随 id,不复制不迁移);admittedIds 不动 ||
82+
| `reassignBinding` | `id = _requireAdmittedId(orch)``bindings[pairId] = id` ||
83+
| `correctVolume` | id 解析 + `_requireAdmittedId`;frozenSince 检查在 id 实体 | **保留**(advance/fpv 覆盖/totalUsd 调整/NotFrozen 约束) |
84+
| `submitShares` | 遍历 `admittedIds``o = r.orchestrators[admittedIds[i]]`**`wallets[count] = o.wallet`(无 _resolve)** | **保留**(usePrev 判定/frozenAtPostEnd 过滤/prevFpv 读取/all-zero no-op/lastSubmittedQ 防重放) |
85+
| `aggregatedFPV` | **无改动**(totalUsd[q] O(1)) | 保留 |
86+
| `bindingOf` | `id = bindings[pairId]``return id == 0 ? address(0) : orchestrators[id].wallet`**_resolve**;unbound 显式返 0) ||
87+
| `fpvOf` | `id = activeIdOf[orch]``id == 0 → return FPV({usd: 0})`;否则读 id 实体 | **保留**(q==activeQ 读 fpv / q==activeQ-1 读 prevFpv / 更早返 0) |
88+
| `isAdmitted` | `id = activeIdOf[orch]; return id != 0 && orchestrators[id].admitted` ||
89+
| `isFrozen` | `id = activeIdOf[orch]; return id != 0 && !(orchestrators[id].frozenSince == Epoch.wrap(0))` ||
90+
| `admittedCount`/`orchestratorCount` | `uint64(r.admittedIds.length)` ||
91+
| `_requireAdmittedId` | **保留**(id 解析辅助:`id != 0 && admitted`,revert NotAdmitted) ||
92+
| `_resolve` | **删除**(调用点 registerPairs/submitShares/bindingOf 已改为直接 id/wallet 读取) ||
93+
| `_swapRemove` | 签名 `address[] → uint64[]`(实现逻辑不变) ||
94+
| `_pairId` | 不变 ||
95+
96+
**唯一外化行为**(与基线一致、与 FIP 文本对齐):replace 后历史季度 FPV 保留(fpv 按 id 存)——基线靠"复制贡献槽"实现,id identity 靠"id 不动"实现,行为目标相同。
97+
98+
### 2.5 测试计划
99+
100+
#### 新增 5 个行为锁定测试(移植早期实现,mirror 语义适配)
101+
102+
helper 命名与新基线测试基类一致(`_admit/_postAs/_rollTo/_qEnd/_qPostEnd/_qVerifyEnd/_correctVolume/_fpv/_pair/_registerPairsAs/_walletShare/_sumShares`,见 `test/SRATestBase.sol`),早期实现 测试几乎原样可移植。
103+
104+
| 测试 | 文件 | 断言要点 | mirror 适配 |
105+
|---|---|---|---|
106+
| `test_Replace_HistoricalQuarterFPV_Kept` | SRAShares | post q0 → replace → submit q0:newOrch 得 1e18、`aggregatedFPV(0)==100e18` | 直接成立(fpv 按 id 存,submitShares(0) usePrev=false 读 fpv) |
107+
| `test_Replace_ShareMap_WritesNewWallet` | SRAShares | 双 orchestrator 各 50 → replace → newOrch 5e17、oldOrch 0、Σ==1e18 | 直接成立 |
108+
| `test_Replace_CorrectVolume_NewAddress_CorrectsHistoricalQuarter` | SRAShares | post 100 → replace → correctVolume(newOrch,0,200) → submit:200 生效 | 成立(activeQ==0 不 advance,`o.fpv=200; totalUsd[0]=+200-100`|
109+
| `test_ReAdmit_FreshIdentity_NoBindingsNoFPV` | SRARegistry | remove → re-admit:pair 可被第三方认领、fpvOf 空 | **fpvOf 断言改**:旧 `!f.posted` 删(新 FPV 仅 usd 字段),断言 `usd == 0` |
110+
| `test_Admit_IdMonotonic_NeverReused` | SRARegistry | nextId 从 1、每次 admit +1、re-admit 不重用 | **slot 计算改**`REGISTRY_SLOT + 3` 低 64 位 = nextId(不再有 admittedCount 打包) |
111+
112+
#### 保留并更新注释的既有测试
113+
114+
- `test_ReAdmit_AfterReplace_FrozenSuccessor_NoShares`(SRAShares T10 回归):断言**不变**(newOrch 冻结 0 份额、oldOrch 1e18),注释从"admit identity reset 清链"更新为"re-admit = 新 id,无链可解析"。
115+
- SRARegistry 既有 replace 测试(`test_Replace_TransfersIdentity` / `test_Replace_AlreadyAdmittedTarget_Reverts` / `test_Replace_OldNotAdmitted_Reverts`):断言需逐一核对——TransfersIdentity 若断言"复制"语义(frozen 转移/历史转移),id 方案下语义等价(跟随 id)但测试内部构造可能依赖 address 键控,tester 核对并同步。
116+
117+
#### invariant handler 改造(移植早期实现 generation 机制)
118+
119+
新基线 handler(`test/SRAInvariant.t.sol`)用 `_successor` 映射模拟 replace 链(4 处:128/154/207-208/356 + resolveHandled 430-431);id identity 融合后改为 **generation 代际机制**(早期实现已验证,移植):
120+
121+
1.`mapping(address => address) _successor` → 加 `mapping(address => uint256) _idGen` + `uint256 _genSeq`
122+
2. `PairRecord``uint256 gen`(绑定时代际)
123+
3. `admit``_genSeq++; _idGen[orch] = _genSeq`(替换 `_successor[orch]=0`
124+
4. `remove`:删 `_successor[orch]=0` 行(id 方案无链可清)
125+
5. `replace``_idGen[newOrch] = _idGen[oldOrch]`(同代际转移);pairs 迁移**仅当前代际**`boundOrch==oldOrch && gen==_idGen[oldOrch]``boundOrch=newOrch`,归档身份的 pair 不迁);`_frozen[newOrch]=_frozen[oldOrch]` 与冻结历史迁移(`_freezeAt/_unfreezeAt`)保留——handler 的冻结模拟是业务语义期望值,与 id 无关
126+
6. `_claimable`:改代际判定——`!_admitted[p.boundOrch] || _idGen[p.boundOrch] != p.gen` → claimable
127+
7. `_setBound`:记录 `gen: _idGen[orch]`
128+
8. I2 invariant:期望值从 `resolveHandled(boundOrch)` 改为 **`boundOrch` 直接相等**(handler replace 已把当前代际 pair 的 boundOrch 同步到新 wallet,与链上 `bindingOf` 返回一致);`resolveHandled` 函数删除
129+
9. `completeParked` 成功分支:`_genSeq++; _idGen[orch] = _genSeq`
130+
131+
handler 的 `_snapshotPostEnd`/`_isFrozenAtHandled`/`_claimable`(读链上 `sra.fpvOf/bindingOf/isAdmitted` 的部分)**不需要改**——它们经链上接口读值,天然适配 id 模型。
132+
133+
#### halmos:无需改动
134+
135+
新基线 `test/halmos/QuarterWindowHarness.sol` 只验证窗口纯函数(`_qEnd/_inPostingWindow/_inVerificationWindow/_afterBinding`),与身份模型无关(mirror 重构已删除冻结区间判定函数)。**与早期实现 不同**(早期实现 需改 harness 冻结判定),本次零改动。验证:`halmos` 跑 QuarterWindowCheck 全绿。
136+
137+
### 2.6 风险与边界
138+
139+
| 风险 | 等级 | 说明与对策 |
140+
|---|---|---|
141+
| **全零季度 bug**(他人并行修复中) | 🟡 | 修复改动集中在 `submitShares`/`_pendingSharesQuarter`(advance 触发条件),与 id identity 的 submitShares 改动(遍历 admittedIds + wallet 读取)同函数不同段。实施时若修复分支已合入,以合入后代码为基线 rebase;本重构 **不修**该 bug |
142+
| **体积 EIP-170** | 🟡 | 基线 runtime 23,938B(余量 638B,`forge build --sizes` 实测)。id identity 融合(每方法 id 解析 + activeIdOf SLOAD)预计 +1~1.5KB,**可能超 24,576B**。对策:仓库既定合约拆分路径(docs/sra-design.md §5.12 #5:logic-to-library / proxy split);实施时实测,超限则标注为已知问题 |
143+
| namespace 不变 + 布局变 | 🟢(文档) | 破坏性存储变更,不支持原地升级;S13 决策记录补充声明(参照早期实现 S13) |
144+
| 归档 id 数据可达性 | 🟢(文档) | remove 后 activeIdOf 清 0,`fpvOf` 不可达归档 id(仅 `bindingOf` 可读归档 wallet);S13 注明归档数据仅链下(事件/索引器)可审计 |
145+
| nextId 溢出 | 🟢 | uint64 递增,0.8.x checked arithmetic revert(治理频率不可达) |
146+
| `_swapRemove` 边界 | 🟢 | 空数组/末位元素逻辑不变(循环查找 + pop),仅类型改 uint64 |
147+
148+
## 3. 验收方案
149+
150+
### 3.1 验收标准与验收步骤
151+
152+
1. **构建**`forge build --sizes` → 编译干净;记录 ServiceRewardsActor runtime 体积(超 24,576B 则标注为 §5.12 已知问题,由合约拆分解决)
153+
2. **单元测试**`forge test` → 全绿(基线 303 + 新增 5 = 308 passed;T10/SRARegistry replace 系列语义同步后无失败)
154+
3. **invariant fuzz**`forge test --match-contract SRAInvariant` → I1/I2/I3/A2/A3 全绿(handler generation 改造后)
155+
4. **halmos**:halmos 跑 `QuarterWindowCheck` → 全绿(零改动应保持通过)
156+
5. **格式与 lint**`forge fmt --check` + `forge lint --deny notes` → 通过
157+
6. **行为锁定**:5 个新测试真实锁定核心外化行为(replace 历史季度连续性、share map wallet 指向、correctVolume 新地址语义、re-admit fresh identity、id 单调性)
158+
159+
## 4. 参考资料
160+
161+
- 早期实现 实现(id 方案参考,基于旧基线):`/tmp/ghost-worktrees/refactor/sra-orch-id/`(src/ServiceRewardsActor.sol、src/lib/SraStorage.sol、test/SRAInvariant.t.sol generation 机制、test/SRAShares.t.sol + SRARegistry.t.sol 5 个测试)
162+
- 新基线:`src/ServiceRewardsActor.sol`(757 行)、`src/lib/SraStorage.sol``test/SRAInvariant.t.sol`(700 行 handler)
163+
- 设计文档:`docs/sra-design.md`(不含 S13;本次实施补充 S13 决策记录,参照早期实现 的 S13 段落改写以匹配 mirror 语义)

0 commit comments

Comments
 (0)