# 链上结算

> 从由数百个分片组成的基础证明，到一份由以太坊合约检验的 Groth16 证明。递归树、判定器的仪式、合约的接口，以及一次部署所固定的内容。

基础证明是由分片证明组成的一个块（block），每个分片证明都是一份 GKR 证明及其承诺：数兆字节的数据和数百个曲线点，任何合约都无法检验。结算分三个阶段压缩它，每个阶段都在仓库根目录下用 `bench` 运行。

> Figure: 结算。每个阶段都验证它的前一个阶段。在合约之前不做任何配对：每个 Mercury 检查都被延迟，并折叠进一个累加器，由合约用两次配对兑现。图中数字来自第 257,510 号区块。

## 1. 递归树

**节点**就是远地虚拟机在证明一个验证者程序。**叶节点**验证一段连续的基础分片；内部节点验证二到四个子证明；**根节点**覆盖全部基础分片。每个节点还会把它的分片和子节点所延迟的每个 Mercury 检查折叠成一对点，因此整棵树最终在顶端归结为单个配对断言。

```sh
# the base proof as an archive: host::proof_archive::write_proof from your host
# program, or `bench prove ... --out <dir>` for the Ethereum guests
cargo run --release -p bench -- recurse <dir>/<stem> --out <out> --in-flight 4
```

`recurse` 写出两个递归程序的密钥，在其上构建叶程序和节点程序的二进制文件，在证明任何东西之前先确定计划（`<out>/tree.txt`：叶节点至多包含 `--leaf` 64 个基础分片，其上的节点至多有 `--fan-in` 4 个子节点），然后逐个节点进行证明。每个节点是一个独立的进程，在证明之前先原生验证其输入，所以错误的输入会被指名拒绝。中断的运行可以恢复：已在 `<out>` 中的证明会被保留，而计划或程序不同的运行会被拒绝。

基础证明完全不受这些影响。叶节点按基础分片的原样验证它们。

## 2. 判定器

根节点仍然是一份 GKR 证明外加数百个点。**判定器**是一个 Groth16 电路，它像节点那样验证根节点，但不做任何折叠。取而代之，它把以下各项绑定为由合约提供取值的线：两个递归程序的程序身份、基础陈述的退出状态、逐字节的公开输入和公开输出（journal），以及根节点欠下一次配对的每个点及其标量。Groth16 证明携带对所有这些线的一个承诺，合约用自己持有的值来检查它。

Groth16 密钥需要一场仪式。第一阶段就是递归树的承诺所依据的同一个 powers-of-tau 文件。第二阶段专属于这个电路，分两轮贡献进行：

```sh
cargo run --release -p bench -- ceremony <out> init          # once per root shape
cargo run --release -p bench -- ceremony <out> contribute    # round 1: alpha and beta, each contributor in turn
cargo run --release -p bench -- ceremony <out> seal
cargo run --release -p bench -- ceremony <out> contribute    # round 2: gamma, delta and eta
cargo run --release -p bench -- ceremony <out> key
cargo run --release -p bench -- decide <out>                 # the Groth16 proof, checked natively and in an EVM
```

每一份贡献都把某个陷门乘以一个只有该贡献者知道的因子，并用一个 Schnorr 证明记录下来，所以任何状态都只需对照电路和仪式文件就能验证。只要某个陷门的贡献者中有一位是诚实的，这个陷门就无人知晓。**各轮的顺序是可靠性的一部分**：在任何东西被 `delta` 或 `eta` 除之前，`alpha` 和 `beta` 就已完成。

> [!CAUTION]
> `bench decide --dev-key` 从一个公开的种子推导出所有陷门，用于开发和测试。在它之下任何人都能伪造证明；它的输出以 `development.*` 的名字写出，以免被误认为来自仪式。在一台机器上跑完的仪式也不算仪式：每一轮都需要一位诚实的贡献者。

`decide` 写出 `decision.constructor` 和 `decision.calldata`：以十六进制表示的部署参数和调用。

## 3. 合约

`contracts/ApogeeVerifier.sol` 只有一个入口：

```solidity
function verify(
    bytes calldata input,        // the base program's public input
    bytes calldata output,       // its journal
    uint256 exitStatus,          // the status you require, normally 0
    uint256[10] calldata proof,  // Groth16 A, B, C and the bound wires' commitment D
    uint256[] calldata points    // x, y and scalar of each point, side [1]_2's then side [x]_2's
) external view returns (bool);
```

它根据 calldata 重建被绑定的值，检查 Groth16 配对等式，用 `ecMul` 和 `ecAdd` 折叠每一侧的点（这同时要求每个点都在曲线上），然后检查折叠后的断言 `e(A, [1]_2) = e(B, [x]_2)`。应用合约调用它，然后根据公开输出采取行动：参见[账本草图](https://apogee.gweb3networks.com/docs/blockchain-native#ledger-native)。

## 一次部署固定了什么

构造函数接收 Groth16 密钥、仪式的两个 G2 点、叶程序和节点程序的程序身份、每一侧的点数，以及**公开输入和公开输出的字节长度**。所以一个已部署的验证者服务于：

- **一个基础程序。** 它的程序身份是叶程序映像中的一个常量，而叶程序的程序身份绑定了这个映像。
- **一种根的形状。** 判定器的电路取决于根节点的程序、它的分片数和公开值的长度，所以密钥及其仪式都是按形状区分的。
- **定长的公开值。** `verify` 会拒绝任何其他长度的输入或公开输出。要把客户程序（guest）设计成链上公开值具有固定大小，例如一条定长记录或一个 32 字节的摘要。

合约为每个点支付约 9,000 gas，因为电路一个点也不折叠。

## 实测数据

第 257,510 号区块；递归树在一台 32 CPU、247 GiB 内存的机器上运行，仪式和判定器在一台 18 核笔记本电脑上运行：

| 阶段 | 结果 |
| --- | --- |
| 基础证明 | 207 个分片，14.5 MB，2,481 s |
| 递归树 | 4 个叶节点（每个至多 64 个基础分片）和一个根节点：共 116 个分片 |
| 叶节点，四个同时进行 | 21、24、23 和 27 个分片；2,157 s；峰值 92 GiB |
| 根节点，同时处理四个分片 | 21 个分片，460 s，1.03 MB |
| 判定器电路 | 7,896,686 个约束，定义域大小为 `2^23` |
| 仪式 | `init` 65 s；每份贡献 50–56 s；`key` 70 s、12.7 GB；密钥 2.65 GB |
| 判定器证明 | 读取密钥 1 s，证明 18.5 s，6.1 GB |
| 合约 | 358 个点；3,620,026 gas；34,980 字节 calldata |

这一切的规范见[递归与判定器](https://apogee.gweb3networks.com/docs/auditors/spec/recursion)。
