# OMNX Chain · 隐私资金通道设计（T3）

适用链：`omnx-1`（ChainID `18888`，HTTP-RPC 8545 / WS 8546，出块 3 秒，gas_price 1）
代码位置：`node/modules_privacy.py`（唯一实质新增）；`node/notes.py`、`node/chain.py` 保持零改动*
自检：`tests/test_privacy_bridge.py`（145 项）；回归基线：`tests/` 十个脚本共 **732** 项

\* `chain.py` 仅新增 1 行白名单键 `privacy`（否则创世里的 privacy 段会被 `load_genesis_config` 过滤掉）。

---

## 1. 一句话说明

T3 之前，隐私层是**独立账本**：`shield` 不扣透明余额、`unshield` 不给透明地址入账，
两者之间没有资金通道。T3 把它打通：**shield 从发起方透明余额扣款、转入托管地址；
unshield 从托管地址转出、给目标透明地址入账**。

## 2. 记账口径

设 `V = public_value`（公开金额）、`F = fee_value`（隐私手续费）、`gas` 按 `gas_price` 计。

| 操作 | 发起方 | 托管地址 `0x…0010` | 手续费池 `fees_pool` |
|---|---|---|---|
| `shield` | `-(V + gas)` | `+(V - F)` | `+F` |
| `transfer` | `-gas` | `-F` | `+F` |
| `unshield` | `-gas` | `-(V + F)` | `+F`；目标透明地址 `+V` |

- Gas 仍由 `Chain._apply_tx` 独立扣取（与隐私手续费是两件事，不重复扣）。
- 隐私手续费与 Gas **同口径**进 `fees_pool`，随区块结算给出块验证者。

## 3. 资金不变量（可独立复核）

```
balances[0x0000000000000000000000000000000000000010] ≡ 未花费 note 面值之和
```

等价写法：`托管余额 == 累计 shield − 累计 unshield − 累计手续费`。

因此：

1. `total_supply()` 仍恒等于创世总量 10 亿（托管地址只是普通账本地址，不新增科目）；
2. 余额变化直接反映在 `eth_getBalance` 上，钱包与区块浏览器都能查；
3. 归档日志 `privacy.log` 每条记录带 `pub: {in, out, fee}`（仅本块总量，不含单笔明细），
   把日志里的 `pub` 加起来应当等于链上托管余额 —— 交叉对账不依赖任何一方自证。

## 4. 开关与创世配置

`deploy/genesis.json`：

```json
"privacy": {
  "bridge_enabled": true,
  "escrow_address": "0x0000000000000000000000000000000000000010",
  "fee_sink": "@fees_pool"
}
```

- 缺省（不写 privacy 段）＝**通道关闭**，链行为与 T1/T2 完全一致；
- 非法 `escrow_address` 会被忽略并回落默认值；
- `escrow_address` 选 `sys_addr(10)`，与既有系统地址段 `1..9`、`A1..B9`、`D1..D4`、`E1..E4`
  以及原有 18 个系统合约地址**均不冲突**；
- 该地址无私钥，只能由链在交易执行中动账，任何人都无法单方面提走托管资金。

## 5. 原子性

执行顺序固定为：**规划（只读）→ 纯校验 → 动账 → 落账**。

- 规划阶段只算「该动哪些账」，不改状态；余额不足、地址非法、托管不足在此直接拒绝；
- 落账（`pool.apply`）抛错时：金额按 `(地址, 增量)` **逆序逐条冲回**，
  Merkle 叶子 / nullifier / 根 / 桩 delta 按长度**整体回退**；
- 结果是二元的：要么「钱动了、note 也铸出/花掉」，要么「什么都没变」，
  **不存在半执行状态**。自检里用注入故障的方式真实验证过这一点。

## 6. 防凭空造币闸门（未背书 note）

通道关闭期间产出的 note **没有透明侧金额背书**。若允许它们走新通道，
等于一开开关就能把旧 note 兑现成真实余额 —— 属于无锚增发。

因此：`bridge_enabled = true` 且池内已有 note、而历史上从未发生过通道动账时，
**第一笔通道交易会被直接拒绝**（错误信息含「未背书 note」）。

处置办法（测试网）：清空 `node/data/privacy.json` 与 `node/data/privacy.log` 后重启节点，
在空池上启用通道。生产环境不允许「先攒 note、后开通道」。

## 7. RPC 接口

| 方法 | 入参 | 返回要点 |
|---|---|---|
| `exx_privacyEscrow` | 无 | `{enabled, escrow_address, escrow_balance, fee_sink, cum_shielded, cum_unshielded, cum_fee, net_escrow, backed}` |
| `exx_privacyInfo` | 无 | 原有 L0 聚合视图 + 新增 `bridge` 小节（同上，只读） |
| `exx_shield` | `secret, owner_pk, value, fee_value?, memo?` | 回执追加 `applied_at_next_block`、`planned{public_value, fee_value, to}`、`bridge`；通道开启时**先做余额预检，不足立即报错** |
| `exx_shieldedTransfer` / `exx_privacyRawTransfer` | `secret, notes[], recipients[], fee_value?` | 同上 |
| `exx_unshield` | `secret, notes[], to, value, fee_value?` | `to` 必须为 `0x` + 40 位十六进制；非法立即报错 |

`to` 地址统一**归一为小写**，避免校验和写法（EIP-55）被记成第二个账户。

## 8. 回滚方案

| 级别 | 操作 | 效果 |
|---|---|---|
| 1（首选） | 创世里 `privacy.bridge_enabled` 改回 `false`，重启节点 | **不改一行代码**退回 T1/T2 语义 |
| 2 | 还原 `node/modules_privacy.py` / `node/rpc.py` 的 `.bak-<时间戳>` 备份，并回退创世 | 回到 T3 之前的代码 |
| 3 | `python3 scripts/omnx-snapshot.py import <快照>` | 状态级回滚 |

⚠️ 已经 shield 出的 note **无法撤销**。关掉开关后 note 仍在链上（可查、可私密转账），
但要再 `unshield` 必须重新打开通道。

## 9. 已知边界（必须如实对外说明）

1. **链上交易仍是明文**：`from` / `to` / `value` 依旧公开；本设计打通的是
   「透明账户 ↔ 隐私 note」的资金通道，**不等于链上交易已加密**。
2. **发送方地址关联性未解决**（无环签名 / zk-SNARK），属 T4 范围。
3. 隐私层的 `fee_value` 目前只走「同口径进 fees_pool」；**隐私版 0.1% 底座费与 70/30 分账尚未接入**，
   属 T5 范围（现口径：shield/unshield 属模块划转，不重复收 0.1% 底座费，
   与链上既有「模块内部划转不重复收取」一致）。
4. 通道交易产生的手续费与 Gas 仍由**明文账户**支付，因此「谁在为隐私交易付手续费」在链上是可见的。
5. 节点升级仍需**停机重启**；这里说的兼容性是协议与状态格式层面的。
6. `exx_privacyNewKey` 由节点生成密钥，**仅供测试网联调**；生产环境必须在客户端本地生成。

## 10. 验收对照（自检覆盖）

- 通道关闭时透明账本零动账，T1/T2 的 97 项一项不回退；
- shield / unshield 余额变化**逐项精确**断言（含 gas 与手续费）；
- 托管余额 == 累计口径 == 日志 `pub` 汇总（三处独立交叉对账）；
- 注入落账故障 → 金额与 Merkle 增量全额回退，指纹与失败前完全一致；
- 篡改 `public_value`、双花、非法 `to`、托管不足、负数、未知类型、未背书 note —— 全部拒绝且状态零变化；
- 重启（日志重放路径）与压实（快照路径）后口径一致；老格式（无 `pub` / 无 `bridge` 段）可正常装载。

## 11. 落盘顺序与断电窗口（2026-09-25 补丁）

资金通道涉及**两份**持久化文件：透明账本 `chain.json`（含托管余额）与隐私日志 `privacy.log`。
两者不是同一次写盘，因此中间存在一个极窄的断电窗口，方向性必须选对：

| 顺序 | 卡在中间的后果 | 性质 |
|---|---|---|
| 旧：先写日志、后写透明账本 | note 已存在、托管余额未入账 → **赤字** | 破坏资金不变量 |
| 新：先写透明账本、后写日志 | 托管余额多出、note 不存在 → **盈余** | 无人可认领，安全 |

修复后固定为**新顺序**，隐私日志是唯一提交点。语义随之收敛为：

- `backed`：**无赤字**（`托管余额 ≥ 累计口径`）。正常链恒为真；为假只代表真的缺钱。
- `backed_exact`：严格对账（`托管余额 == 累计口径`）。盈余时该值为假，可用 `escrow_surplus` 定位。
- `escrow_surplus` / `escrow_deficit`：盈余 / 赤字的具体金额。

对应回归用例：`tests/test_privacy_durability_order.py`（21 项），
其中第 4 组**反向**模拟「旧顺序产物」，要求必须报出 `backed=false`，
以防这次放宽把检查变成永远为真。
