# 流式证明者

> 证明者把客户程序执行两遍，从不持有执行轨迹。内存取决于正在处理的分片，而不是运行的长度；证明也不依赖于调度。

一个完整的以太坊区块约有 2 亿个周期。它的执行轨迹，即每个电路族的每一行和每一个内存事件，每个周期约 300 字节：在承诺任何一列之前就已达到数十 GB。远地虚拟机的证明者从不构建它。它把客户程序（guest）执行两遍，只持有正在处理的分片。

## 两遍执行

> Figure: 先承诺，再证明。内存挑战必须在每个分片的内存承诺之后产生，所以承诺先来自一次执行，证明再来自第二次执行。

**第一遍**执行客户程序，每填满一个分片，就承诺它的内存列，保留承诺，丢弃行数据。退出时，它从最终的内存状态推导出陈述所需的其余一切：寄存器与 pc 的边界、这次运行触及的内存窗口列表，以及窗口电路族的分片。随后它运行全局 transcript：吸收陈述（包括每一个内存承诺），并抽取内存挑战，以及作为每个分片种子的摘要。

**第二遍**再次执行。模拟器是其输入的纯函数，所以切出的分片完全相同，第二遍还会断言自己的周期分布、窗口列表和边界与第一遍相同。每个分片都得到它的全部已承诺列并被证明：它的 transcript、它的 GKR 过程、它的打开。内存列不会被重新承诺：打开从陈述中取得它们的承诺，从第二遍取得它们的值，所以如果两遍之间某些列不一致，得到的打开会被验证者拒绝。

这个顺序是可靠性所强制的。内存挑战必须在内存元组可能读取的每个值之后产生，所以每个分片的内存列都要在任何分片能够被证明之前承诺。

## 流水线

固定数量（`max_in_flight`）的工作线程共用一把保护执行器的锁。持有锁时，工作线程交回已完成的分片并认领下一个：如果有已填满的分片在等待，就认领它；否则它自己推进执行器，直到某个缓冲区填满。在锁外，它构建该分片的列、证明它，然后丢弃它。

- **执行器从不超前于需求运行。** 每个电路族至多有一个已填满、未被认领的分片以行的形式等待。
- **在一个分片内部，工作是数据并行的**，分布在所有核心上。阻塞在这项工作中的工作线程不会再接第二个分片。
- **块（block）不依赖于调度。** 分片的证明是全局状态和它自身各列的函数；证明按其在陈述中的位置放置。同时处理 1 个和 8 个分片时，得到的字节完全相同。
- **失败是确定性的。** 无论工作线程有多少，返回的都是按填充顺序最早的那个失败。

## 成本

内存由以下几部分构成：每个电路族一个未填满的缓冲区、最近访问表、正在处理的分片，以及输出。一个分片的工作集以其前向过程为主，即以域元素形式存放的每个 GKR 内部层：一个 `2^20` 的 `SHIFT_BITWISE` 分片为 8.4 GiB，一个 `2^18` 的 `KECCAK_F` 分片为 42 GiB。所以 **`max_in_flight` 决定了有多少个这样的工作集同时存在，而高度决定了每个有多大。**

在第 257,510 号区块（60 笔交易，101.5 Mgas，198M 个周期，207 个分片）上实测，机器为 32 vCPU、247.7 GiB 内存，同时处理十二个分片：

| | |
| --- | --- |
| 第一遍 | 191 s；单线程填充占其分片秒数的 81%；采样内存至多 15.9 GiB |
| 第二遍 | 2,290 s；客户程序退出之前，12 个分片名额中约有 12 个被占用，30.4 个 vCPU 处于忙碌；随后是 460 s 的尾段 |
| 内存峰值 | 173.92 GiB：两个 `2^18` 的 `KECCAK_F` 分片，在尾段同时处理，此外再无其他分片在处理中 |

峰值来自某一个委托电路族的高度，而不是十二个同时处理中的分片。这就是可调节之处：Keccak 调用更少的区块，或者把 Keccak 设在更低的高度，峰值就更低。

规范见[流式证明者](https://apogee.gweb3networks.com/docs/auditors/spec/streaming)。
