# 证明与验证

> 登记程序、证明一次运行、验证块，并保存证明。高度、同时处理中的分片、证明所需的仪式幂次，以及验证者必须自己持有的两个值。

## 三个调用

```rust title="设置、证明、验证"
let params = program::ProgramParams::defaults();
let ptau = std::path::Path::new("assets/ptau/ppot_0080_24.ptau");
let srs = srs::Srs::from_ptau(ptau, 22).expect("ceremony");

let setup = host::setup(&elf, &params, srs).expect("registers");          // once per program
let proven = host::prove(&setup, &io, 4).expect("proves");                // at most 4 shards in flight
host::verify(&setup.vk, &proven.block).expect("verifies");

assert_eq!(setup.vk.identity.to_bytes(), registered); // from your own channel, never the proof
assert_eq!(proven.exit_code, 0);
```

- **`host::setup`** 加载 ELF，把它解码为各电路族的表和 `VmConfig`，在仪式之下承诺设置列，并构建验证密钥。它的成本按程序和高度选择计算，而不是按运行计算。
- **`host::prove`** 把客户程序（guest）执行两遍，并证明每个分片（[见下文](#two-passes)）。它返回一个 `Proven`：`BlockProof`、退出码、周期数、公开输出（journal）以及这次运行的报告。
- **`host::verify`** 用块（block）所携带的陈述，对照密钥检查该块。它不会拿程序身份或 SRS 摘要与任何东西比较，所以这项比较要由你来做。

[快速上手](https://apogee.gweb3networks.com/docs/launch/quickstart#prove)在一个小型客户程序上运行的正是这段代码，并附有真实输出。

## 验证者必须自己持有的值

有两个值必须来自证明者无法控制的渠道：

1. **程序身份。** 如果对照的是证明者提供的身份，证明只能说明*某个*程序运行过。验证者登记它所信任的发布版本的程序身份，并与密钥中的程序身份比较。
2. **仪式的 SRS 摘要。** 不论密钥自身的点给出什么摘要，密钥都会按这个摘要加载。基于已知 `τ` 构建的密钥可以打开任何东西，只有把它的摘要与仪式的摘要比较，才能拒绝它。

验证密钥本身可以来自任何人，包括证明者：加载时会根据其自身内容重新计算程序身份和 SRS 摘要，并要求其中的电路与验证者自己的注册表一致。然后读取陈述：先读退出状态，再读公开输出。

## 高度

每个电路族都有一个**高度**，即它一个分片中的行数，从 `2^8, 2^12, 2^16, 2^18, 2^20, 2^22` 中选取。高度属于程序，而不属于某次运行：每个高度都被绑定进程序身份。

| 电路族分组 | 默认值 | 下限 | 说明 |
| --- | --- | --- | --- |
| 七个指令电路族 | `2^22`；`MUL_DIV` 和 `ATOMICS` 为 `2^20` | `2^20` | 其时间戳范围检查的下限 |
| `INIT_TEARDOWN`、`ZERO_WINDOWS`、`ADVICE_WINDOWS` | `2^22` | `2^16` | 共用一个窗口高度；窗口 0 必须容纳映像中每个来自文件的字节 |
| `PUBLIC_INPUT`、`PUBLIC_OUTPUT` | `2^12` | 固定 | 高度决定了它们窗口的位置 |
| 委托电路族 | 见[委托](https://apogee.gweb3networks.com/docs/launch/delegations#cost) | 因电路族而异 | |

一个有行的电路族，至少要花费一整个其高度的分片，所以短的运行在较小的高度下浪费更少，长的运行在较大的高度下需要的分片更少。解码表还必须足够高，才能覆盖到该电路族的最后一条指令：`2^20` 可覆盖 1.9375 MiB 代码，`2^22` 可覆盖 7.9375 MiB。以太坊客户程序对每个高度可选的电路族都使用 `2^20` 进行证明。

```rust title="指令电路族取其下限高度，RAM 窗口取 2^16"
use constants::family;

let mut params = program::ProgramParams::defaults();
for f in 0..7 {
    params.heights[f] = 1 << 20;
}
for f in [family::INIT_TEARDOWN, family::ZERO_WINDOWS, family::ADVICE_WINDOWS] {
    params.heights[f as usize] = 1 << 16; // window 0 is then 256 KiB: the image must fit in it
}
```

仪式提供的幂次数必须不少于最高的电路族的行数，并且至少为 `2^18`，以满足通用查找（lookup）表的需要：调用 `Srs::from_ptau(path, k)` 时，`2^k` 至少要等于最大的高度。

## 同时处理中的分片

`host::prove` 的第三个参数是 `max_in_flight`，即同时证明的分片数。它是唯一一个在内存与时间之间权衡的旋钮：

- **内存取决于同时处理中的分片**，而不是周期数。每个同时处理中的分片都持有自己的行、前向过程，以及不断增长的证明。最宽的指令电路族的一个 `2^20` 分片，在前向过程中约占 8.4 GiB；一个 `2^18` 的 `KECCAK_F` 分片约占 42 GiB。
- **时间取决于有多少分片并行运行**，最多到你拥有的核心数为止。在一个分片内部，工作在所有核心上运行。
- **证明与它无关。** 同时处理 1 个和 8 个分片时，得到的块逐字节相同。

在笔记本电脑上从较小的值开始，两个或四个；在服务器上逐步调高，直到限制你的是内存而不是核心数。`bench prove` 的默认值是 8。

## 两遍执行

`host::prove` 以流式方式工作。它从不持有整个执行轨迹；按每个周期约 300 字节计算，执行轨迹会是系统中最大的对象。

1. **第一遍**执行客户程序，每填满一个分片，就承诺它的内存列，保留承诺，丢弃行数据。到退出时，它构建陈述，并抽取所有分片共用的挑战。
2. **第二遍**再次执行。模拟器是确定性的，所以切出的分片完全相同。每个分片一到达就被填充、证明并丢弃，只保留它的证明。

这就是为什么证明要花两次执行的时间，而内存以同时处理中的分片为界。[流式证明者](https://apogee.gweb3networks.com/docs/architecture/streaming)对此有深入的解释。

## 保存证明

`host::proof_archive::write_proof(dir, stem, vk, block)` 写出四个文件，每个文件都是对应类型的原始字节：

```text
<stem>.vk         the verifying key
<stem>.identity   the key's identity, 64 lowercase hex digits and a newline
<stem>.public     the statement: input, journal, exit status, the execution's record
<stem>.block      the block proof
```

`read_proof(dir, stem)` 把它们读回来。`.identity` 文件记录的是这次运行所声称的身份；验证者仍要与自己的副本比较。`verifier` 命令行工具可以检查一份归档：

```sh
cargo run --release -p verifier -- block <stem>.vk  <stem>.public <stem>.block
```

全部验证通过时它以 0 退出；以 1 退出时会指出第一处拒绝；用法错误或程序身份格式错误时以 2 退出。它把你传入的程序身份与密钥中的比较，SRS 摘要则取自密钥文件。

## 当证明失败时

诚实的证明者永远不会生成无法通过验证的证明，所以一旦失败，要么是它接受了不该接受的输入，要么是存在 bug。打开证明者的调试日志重新构建，然后重新运行：

```sh
cargo run --release -p bench --features prover/debug-info -- prove ...
APOGEE_DEBUG=detail <the same run> 2>&1 | tee run.log
grep -E 'FAIL|NOT CANONICAL|UNBALANCED|OVER the|NAMES NO|DISAGREES|NOT LOOPING|ABORTED' run.log
```

日志会指出失败的分片；`self_check FAILED` 会指出某一行违反的第一个门，并给出每个操作数的值。[工具](https://apogee.gweb3networks.com/docs/reference/tools#s3)列出了所有标记。日志只存在于启用了 `debug-info` 特性的构建中，不会改变证明的任何字节。

下一步：[链上结算](https://apogee.gweb3networks.com/docs/launch/on-chain)。
