# OMNX Chain · 链升级兼容性规范（平滑升级 · 第 4 项）

> 建立于 2026-09-24。**目的：把「什么改动可以直接上、什么必须先做分叉预案」写成可执行的口径**，
> 而不是每次升级靠人拍脑袋。
>
> ⚠️ 本次仍为**测试网**，主网闸门保持关闭；对外口径：HTTP Gossip + 单验证者确定性轮转，
> **不具备拜占庭容错**，严禁描述为 BFT 共识。

---

## 一、三条铁律

1. **加字段 ≠ 改口径**：新增字段一律走 `x_` 扩展命名空间，**不参与哈希**；
   只有「提升为正式字段」才进哈希，而那时必须**同时升 `v`**。
2. **广播宽松、落账 closed**：`add_tx_external` 认得就收（不认识的版本也先入池转发）；
   `_apply_tx` 只执行 `MIN_APPLY_V ≤ v ≤ MAX_APPLY_V`，**绝不半懂半执行**。
3. **旧程序读新文件必须被拦住**：状态文件带 `min_reader_v`，读不懂就**拒绝启动**，
   不允许「硬读」—— 硬读出来的状态是坏的，而且坏得没有声音。

---

## 二、交易信封

| 字段 | 含义 | 参与哈希 |
|---|---|---|
| `v` | 信封版本号。当前 `ENVELOPE_V = 1` | ✅ |
| `x_*` | 扩展命名空间（保留前缀） | ❌ |
| `hash` / `ts` / `ok` / `error` / `contract` / `result` / `trades` / `order_id` / `intent_id` | 运行时字段（接收方/打包方填充） | ❌ |

- **缺 `v` 视为 `v1`**：升级前产生的交易没有这个字段，但格式与 v1 一致。
  当成 0 会把历史交易判成非法 —— 这是最容易踩的坑。
- `v` 在 `Chain.add_tx()` 里、**签名与算哈希之前**写入，因此它被签名与哈希一起覆盖。
- 版本裁决：

| 阶段 | 函数 | 口径 |
|---|---|---|
| 广播（收到对端交易） | `tx_envelope.gossip_gate()` | 结构认得就收；版本不认识也入池、转发 |
| 落账（打包执行） | `tx_envelope.apply_gate()` | `v > MAX_APPLY_V` → 记 `ok=False` + `error`，**不执行** |

---

## 三、哈希口径与 `x_` 扩展命名空间

**口径**：`参与哈希的字段 = 全部字段 − 运行时字段 − x_ 扩展字段`

为什么不改成「显式白名单」：白名单要求穷举历史上出现过的每一个交易字段
（`module` / `op` / `token` / `pair` / `amount` / `minOut` / `symbol` / `to` / `zone` / `tier` /
`params` / `data` / `nonce` / `gas` / `gas_price` / `value`…），分散在 dex / platform /
rwadex / privacy / evm 各层。**漏掉任何一个，那一类交易的哈希就会变，等于把正在跑的链判成非法。**
所以走加法：只额外挖掉 `x_`。当前没有任何字段以 `x_` 开头，因此对现存交易，
新旧算法算出的哈希**逐字节相同**（`tests/test_upgrade_compat.py` 第 2 节有对照）。

### `x_` 字段的纪律（因为它不参与哈希 ⇒ 天然未签名）

- ✅ 允许：纯提示、纯路由、不影响任何状态的旁路数据（例如 `x_client`、`x_trace`）。
- ❌ 禁止：任何会改变账本/合约状态的扩展。这类必须**升 `v` 并提升为正式字段**（那时它进哈希）。
- 本节点**不生产**未申报的 `x_` 字段：`tx_envelope.stamp()` 会直接拒绝，
  免得我们亲手造出一笔「字段没被签名保护」的交易。
- 接收侧保持宽松：认得就认，不认得当没看见。这是软升级能成立的前提。

---

## 四、状态文件（`chain.json`）

| 键 | 含义 |
|---|---|
| `state_v` | 写入方使用的状态格式版本。当前 `STATE_V = 1` |
| `min_reader_v` | 读懂这份文件所需的最低读取器版本 |
| `chain_id` / `chain_id_num` | 链标识（防止跨链快照被误导入） |

- 读取闸门：`tx_envelope.reader_gate()`。`min_reader_v > READER_V` → **拒绝启动**。
- **向前兼容**：新版本新增的键，旧程序的 `_load()` 一律忽略（`data.get(k, 默认值)`），
  因此「新程序写、旧程序读」在**同 `state_v`** 内是安全的；
  一旦格式变到旧程序读不了，**必须**抬 `min_reader_v`。

---

## 五、改动分类表（照这张表判断，别凭感觉）

### A 类 · 状态兼容（可以直接上，不需要分叉预案）

| 改动 | 为什么安全 |
|---|---|
| 新增只读 RPC 方法 | 不改状态，旧节点没有该方法而已 |
| 新增 `x_` 扩展字段 | 不参与哈希，旧节点忽略 |
| 新增状态键（`chain.json` 里多一个键） | 旧程序 `_load()` 用 `.get()`，忽略未知键 |
| 新增独立模块 / 独立状态文件（如 `privacy.json`） | 老代码读写不到，天然隔离 |
| 修 bug 且不改变既有交易的执行结果 | 不产生状态分叉 |
| 只改日志、文档、前端页面、脚本 | 与共识无关 |

### B 类 · 条件兼容（能上，但必须按顺序、且要观察）

| 改动 | 前提条件 |
|---|---|
| 新增模块交易（新 `module` / 新 `op`） | 全节点先升级到支持该模块的版本，再开始发这类交易；旧节点会落账失败（`ok=False`），不会静默改状态 |
| 新增系统合约地址 | 必须走创世或治理提案，且全节点一致 |
| 调整手续费/分账等**参数** | 参数必须通过治理提案下发，不能靠改代码 |

### C 类 · 不兼容（**必须先出分叉预案，禁止直接上**）

| 改动 | 为什么 |
|---|---|
| 改变**既有字段**的语义或取值口径 | 新旧节点对同一笔交易算出不同的状态 → 分叉 |
| 把 `x_` 字段提升为正式字段（进哈希） | 新旧节点对同笔交易的哈希判断不同 |
| 改动区块结构（`index` / `prev` / `state_root` 计算方式） | 直接断链 |
| 改动创世分配 / 总量 / 链 ID | 直接分叉 |
| 删除或重命名既有状态键 | 旧程序读到 `None`，可能静默算错 |
| 抬 `min_reader_v` | 旧程序将无法启动（这是**故意**的，但必须提前通知所有节点） |

---

## 六、C 类改动的分叉预案模板（每次都要填）

```text
改动名称：
分类：C 类（不兼容）
生效高度 / 时间：
需要升级的主体：全部验证者 / 全部 RPC 节点 / 交易所适配器 / 区块浏览器
升级顺序：先全部验证者 → 再 RPC 节点 → 最后上层应用
切换点：第 N 块（提前公告）
旧链处置：停止出块 / 仅只读（选一个，写清楚）
数据处置：快照备份（scripts/omnx-snapshot.py export）→ 校验 → 导入新链
回滚点：快照文件路径 + 高度 + 链头哈希
对外公告：提前 ≥ 24h，写明影响范围
验收：新链高度 > 切换点 且 连续出块 ≥ 100 块
```

---

## 七、升级操作流程

### 本地（桌面运行目录）

```bash
# 1) 备份（改动前）
cp node/chain.py node/chain.py.bak-$(date +%Y%m%d-%H%M%S)
python3 scripts/omnx-snapshot.py export

# 2) 停节点 → 替换代码 → 启节点
bash 停止OMNX测试网.command
#    ... 替换 node/*.py ...
bash 启动OMNX测试网.command

# 3) 对账
python3 scripts/omnx-snapshot.py info
bash deploy/aws/run-local-gate.sh      # 全量回归闸门
```

### AWS

见 `docs/蓝绿部署方案.md`。核心顺序：
**备用实例上先跑通 → 验完再切流量 → 出问题秒切回旧实例**。

---

## 八、回滚

| 场景 | 动作 |
|---|---|
| 只是代码改动 | 还原 `*.bak-YYYYMMDD-HHMMSS`，重启节点 |
| 状态被写坏 | `python3 scripts/omnx-snapshot.py import <完整快照> --data <DIR>`（导入前自动校验；被覆盖的文件会留 `.bak-<ts>`） |
| 快照本身有问题 | `python3 scripts/omnx-snapshot.py verify <包>` —— 校验不过**一律拒绝导入** |
| 节点还在跑 | 导入会被拒绝（除非 `--force`）。**先停节点**，否则内存态会覆盖回磁盘、白导 |

⚠️ **恢复必须用完整快照**。`backup` 产出的增量区块包只用于归档与核对：
余额 / nonce / 合约存储是**执行结果**，链路没有「从创世重放」的实现，重放不出来。

---

## 九、并入快照的状态文件（一个都不能少）

| 文件 | 内容 | 漏掉的后果 |
|---|---|---|
| `chain.json` | 链账本（余额 / nonce / 合约存储 / 区块 / 平台与 DEX 状态） | 全丢 |
| `evm_state.json` | EVM wei 小数余额叠加层 | **ETH 侧余额回退**（最容易漏） |
| `privacy.json` | 隐私账本快照 | 隐私账本回到上一个压实点 |
| `privacy.log` | 隐私账本追加日志 | 隐私账本丢失自上次压实以来的增量 |

---

## 十、当前版本基线（2026-09-24）

| 项 | 值 |
|---|---|
| `ENVELOPE_V` | 1 |
| 允许落账的版本区间 | `[1, 1]` |
| `STATE_V` | 1 |
| `READER_V` / `MIN_READER_V` | 1 / 1 |
| 扩展前缀 | `x_`（当前无已申报字段） |
| 哈希口径 | 全部字段 − 运行时字段 − `x_` 扩展字段 |
| 兼容性自检 | `tests/test_upgrade_compat.py` |

> 诊断入口：`python3 scripts/omnx-snapshot.py info` 会打印本地链高度、
> `state_v` / `min_reader_v` 与完整口径。
