本文深入分析了 Starkware 开源的 Stone Prover,一个使用 STARKs 技术生成计算完整性证明的 C++ 库。
大约一个月前,Starkware 开源了其 Stone Prover,该验证器目前在 Starknet 中投入生产。它是一个允许使用 STARK(可扩展的透明知识论证)生成计算完整性证明的库。
该代码库大约有 10 万行代码,主要用 C++ 编写。它有以下主要组件:
在 Lambdaclass,我们正在开发我们的 Cairo prover,STARK Platinum(用 Rust 编写),使其与 Stone Prover 兼容,以便任何人都可以使用 Rust 版本为构建在 Starknet 之上的不同应用程序生成有效的证明。我们希望我们的 prover 的性能和可用性能够帮助社区采用它。
在这篇文章中,我们将分析 Stone Prover 的一些组件,并解释它们的工作原理和实现。有关 STARK 的介绍,请参阅我们之前的文章或 STARK Platinum 文档。
每个实现的域 FF 都有一个单位群 F×F× 的生成器 ωω。它们可以通过调用 PrimeFieldElement
的类方法 Generator
获得。Stark252Field
的生成器是 ω=3ω=3(这里 和 这里)。
表示域的类是 ListOfCosets
。TraceGenerator
方法返回一个 trace length 2n2n 阶的单位本原根 gg,它生成一个域 DD。它被计算为 g=ω(p−1)/2ng=ω(p−1)/2n。然后,LDE 表示为一系列陪集 hiwD:i=0,…,k−1hiwD:i=0,…,k−1,所有陪集的大小与 DD 相同,这样它们的并集就是实际的 LDE 域:
DLDE=wD,∪,hwD,∪,h2wD,∪,⋯,∪,hk−1wD,DLDE=wD,∪,hwD,∪,h2wD,∪,⋯,∪,hk−1wD,
其中 h=w(p−1)/2n+kh=w(p−1)/2n+k。
Stone prover 使用 NonInteractiveProverChannel
类来处理其与 transcript 的交互。有两个基本操作:
SendBytes
:prover 将字节附加到 transcript。ReceiveBytes
:prover 从 transcript 接收字节。这些操作是更复杂操作的构建块,例如采样 FieldElement
或数字。可以使用多种哈希函数与 transcript 交互(例如,Keccak、Pedersen)。
这些操作主要在 HashChain
类中实现,其他类只是委托给它。它具有以下属性:
self.counter
:计算已使用的 KK 字节块的数量。self.hash
:保存哈希函数的当前状态。self.spare_bytes
:当用户请求 TT 字节,而 TT 不是 KK 的倍数时,它会存储它们以供以后使用。这里,KK 是存储所选哈希函数输出所需的字节数(例如,Keccack256 为 32 字节)。
当字节 XX 附加到 transcript 时,获取当前摘要 DD 并将其解释为 BigInt。然后,将 seed increment 添加到其中。这个新 seed 和 XX 的连接是哈希函数的最新状态。
伪代码:
def append(new_bytes, seed_increment):
digest = self.hash.digest()
new_seed = digest + self.seed_increment
self.hash = Hash(new_seed || bytes)
这里,||
是连接运算符。
伪代码:
def sample_block():
counter_bytes = | 24 bytes of 0x00 | counter as u64 | # This depends on the "block" size, the hash size.
self.counter++
return Hash(self.hash.digest() || counter).digest()
def sample(number_bytes):
for chunk32 in split(number_bytes, 32):
result = result + sample_block()
return result
这是一个简化版本的代码。这里,假设哈希大小为 32 字节(256 位)。此外,此伪代码不处理程序员请求的字节数不是哈希大小的倍数的情况。
主要的prover和verifier可执行文件使用 Fiat-Shamir 策略初始化 transcript。这意味着使用公共参数更新哈希函数。
有两种实现:Fibonacci AIR 和 Cairo AIR (CpuAir
)。
claimed_index_in_64_bit_big_endian || claimed_value_in_montgomery_form
初始化n_steps
、rc_min
、rc_max
和公共内存初始化。布局在此处描述here。当执行主prover时,可以启用标志 -generate_annotations
。这会记录 prover 和 verifier 之间的交互,并有助于调试和解决兼容性问题。注释将添加到证明的输出 JSON 文件中。
默认情况下,使用 keccak256
哈希函数。
这是支持的选项列表:
using HashTypes = InvokedTypes<
Blake2s256, Keccak256, Pedersen, MaskedHash<Keccak256, 20, true>,
MaskedHash<Blake2s256, 20, true>, MaskedHash<Blake2s256, 20, false>,
MaskedHash<Keccak256, 20, false>>;
这里 是如何实例化 composition polynomial 并计算其 evaluations 的示例。它可以像 Fibonacci 示例一样运行。
CompositionPolynomial
:定义接口的抽象类。它只有两个子类
CompositionPolynomialImpl
:上述的具体实现。它不遵循 pimpl 模式。它只是一个子类。CompositionPolynomialMock
:用于测试。CompositionOracleProver
:CompositionPolynomial
的一个包装器,它也知道 interpolation 的 polynomial(称为 traces
)、interpolation 的域和 transcript。尽管名称如此,但类 CompositionPolynomialImpl
不负责 composition polynomial 的实际计算。它不处理收集约束的所有单独 evaluations 并将它们粘合在一起以形成 composition poly 的逻辑。它处理并行化并格式化所有输入以将它们传递给 Air::ConstraintsEval
。这是 constraints 被 evaluating和聚合以获得 composition polynomial 的 evaluation 的方法。因此,Air
的每个实现都负责所有 constraint evaluations 的正确聚合步骤。
有两件事很突出:
为了计算 composition polynomial evaluations,prover 调用 CompositionOracleProver::EvalComposition(n_tasks)
。这将返回 dd cosets 中 composition polynomial 的 evaluation 集,其中 dd 被选择为最小整数,使得 composition polynomial 的 degree bound 小于 2nd2nd(有关域和陪集的详细信息,请参见 域 部分)。然后,oracle 使用其指向 trace polynomials 的指针在 LDE 域中 evaluating 它们(或使用来自先前 commitment 阶段的缓存计算)。然后,oracle 将其与陪集偏移量和其他域相关数据一起传递给 CompositionPolynomial::EvalOnCosetBitReversedOutput()
。此方法启动多个任务,这些任务调用 Air::ConstraintEval
以计算 LDE 上的一个点的单个 evaluation。这是最终完成计算的方法。
组合polynomial HH 总是被分成 d=deg(H)/2nd=deg(H)/2n 部分,其中 2n2n 是trace长度,
H=H0(Xd)+XH1(Xd)+⋯+Xd−1Hd−1(Xd).H=H0(Xd)+XH1(Xd)+⋯+Xd−1Hd−1(Xd).
为此,在计算 HH 的 evaluation 之后,计算每个 HiHi 的一种方法是插值 HH,然后在其系数上拆分单项式基。Stone prover 中的该方法是对此的优化。他们只执行 log(d)log(d) 步 IFFT,而不是运行完整的 IFFT 来插值 HH,如果 dd 是 2 的幂,则得到每个 HiHi 的 evaluations。
一个奇怪的设计选择是重用 AIR 和 composition polynomial 机制来构建 DEEP composition polynomial。deep composition polynomial 被视为特定 AIR 的 composition polynomial,称为 BoundaryAIR
(参见 这里 和 这里)。它与边界约束无关。它仅用于构建 DEEP composition poly。它是同一个类,与用于算术化要证明的程序的 FibonacciAIR、CpuAir 或任何其他 AIR 无关。deep composition polynomial 在主要的 ProveStark
方法中称为 oods_composition_oracle
。
这样做的一个副作用是使注释变得混乱。看起来 verifier 选择了两次用于构建 composition polynomial 的 challenge:
...
V->P: /STARK/Out Of Domain Sampling: Constraint polynomial random element
...
V->P: /STARK/Out Of Domain Sampling: Constraint polynomial random element
第一次指的是 composition polynomial 的 challenge。第二次指的是构建 DEEP composition polynomial 的 challenge。
commitment 算法在其核心很简单,但是许多类相互交互以生成 commitment。此外,还有一些设置和其他因素可以改变 commitment 的方式。例如,由于其大小,在 LDE 上 evaluating 的 trace 可能不适合 RAM,从而改变了 commitment 策略。让我们首先分析核心算法,假设 LDE 适合 RAM 并且不使用任何特殊设置。
这里的策略是构建一个 Merkle 树。为了生成这个 Merkle 树,我们需要知道如何制作树的叶子以及如何将两个节点合并为一个节点以用于下一层。
假设我们要提交以下trace:
LDE上第 1 列的 Evaluations | LDE上第 2 列的 Evaluations |
---|---|
t0(wh0g0)t0(wh0g0) | t1(wh0g0)t1(wh0g0) |
t0(wh0g1)t0(wh0g1) | t1(wh0g1)t1(wh0g1) |
t0(wh0g2)t0(wh0g2) | t1(wh0g2)t1(wh0g2) |
t0(wh0g3)t0(wh0g3) | t1(wh0g3)t1(wh0g3) |
t0(wh1g0)t0(wh1g0) | t1(wh1g0)t1(wh1g0) |
t0(wh1g1)t0(wh1g1) | t1(wh1g1)t1(wh1g1) |
t0(wh1g2)t0(wh1g2) | t1(wh1g2)t1(wh1g2) |
t0(wh1g3)t0(wh1g3) | t1(wh1g3)t1(wh1g3) |
我们参考 域 部分以了解域详细信息和陪集。整个 LDE 域由 ww 偏移,hh 的幂表示该值所在的陪集,而 gg 的幂表示该陪集内的索引。在提交 trace 之前,stone prover 会置换行的顺序。
首先,following 按位逆序对陪集进行置换。例如,如果我们有:
| coset 1 | coset 2 | coset 3 | coset 4 |
应用按位逆序置换:
| coset 1 | coset 3 | coset 2 | coset 4 |
然后,再次应用按位逆序,但要在每个陪集内单独应用。最终的置换 trace 将如下所示:
LDE上第 1 列的 Evaluations | LDE上第 2 列的 Evaluations |
---|---|
t0(wh0g0)t0(wh0g0) | t1(wh0g0)t1(wh0g0) |
t0(wh0g2)t0(wh0g2) | t1(wh0g2)t1(wh0g2) |
t0(wh0g1)t0(wh0g1) | t1(wh0g1)t1(wh0g1) |
t0(wh0g3)t0(wh0g3) | t1(wh0g3)t1(wh0g3) |
t0(wh1g0)t0(wh1g0) | t1(wh1g0)t1(wh1g0) |
t0(wh1g2)t0(wh1g2) | t1(wh1g2)t1(wh1g2) |
t0(wh1g1)t0(wh1g1) | t1(wh1g1)t1(wh1g1) |
t0(wh1g3)t0(wh1g3) | t1(wh1g3)t1(wh1g3) |
在这种情况下,我们只有两个陪集,因此应用按位逆序没有任何作用,并且这两个陪集保持在相同的位置。然后,重新排序每个陪集内的元素。现在我们有了正确的顺序,我们可以开始构建 Merkle 树的叶子。
每个叶子将对应于一行。这是因为每次 prover 打开 ti(z)ti(z) 时,它都会在相同的值 zz 处打开所有其他列 tj(z)tj(z),因此将它们存储在同一叶子并对它们使用相同的身份验证路径是有意义的。
如果每列都有 |LDE||LDE| 行,我们将有 |LDE||LDE| 个叶子,每个叶子都有其哈希。第 ii 个叶子是通过哈希第 ii 行中所有列的连接而得到的哈希。因此,例如,本例中的第一个叶子是 H(t0(wh0g0)||t1(wh0g0))H(t0(wh0g0)||t1(wh0g0))。
请注意,stone prover 将其 field elements 存储在蒙哥马利形式中,以提高其运算的性能。当使用 field element 的字节对其进行哈希时,field element 保持为蒙哥马利形式(它不会转换为标准格式)。此外,表示 field element 的 limbs 从最低有效位存储在位置 0,到最高有效位存储在末尾。
现在我们有了叶子,我们的树的第一层,我们可以通过合并节点来构建下一层。为此,Stone Prover 通过连接其哈希并获得新父级的哈希来连接两个连续的节点。重复此操作会将每个步骤中的节点数减半,直到 Merkle 树完成。
有关一个简单的示例,请查看 python 代码。
查看 Fibonacci 示例,了解如何实例化与 commitments 相关的类。
TableProver
抽象类及其实现 TableProverImpl
是用于处理 field elements 的二维数组的 commitments 和 decommitments 的高级接口。它主要由一个 commitment scheme 组成,但也有一个指向 ProverChannel 的指针,用于从 verifier 发送和接收元素。
有一个 TableProverFactory 和一个 utils function 来实例化它。还有一个在 测试 中使用的 helper。
TableProverImpl
有一个 Commit
方法,该方法又调用其 commitment_scheme_
成员的 Commit
方法,该成员是指向 CommitmentSchemeProver
的指针。
此类 实现了 commitment scheme 的逻辑。
有一个 commitment scheme 构建器,它调用另一个方法 这里,该方法通过 交替调用PackagingCommitmentSchemeProver
和 CachingCommitmentSchemeProver
来构造 CommitmentSchemeProver
。
处理太大而无法装入 RAM 的 traces 或 LDE 时,需要考虑多个细节。
多项式在 LDE 上的 evaluations 被分成段。每个段都包含行的连续子集。为每个部分构建一个 Merkle 树。然后,在此之上构建另一个 Merkle 树,其中叶子是每个段的 Merkle 树的根。
prover 可能希望在 commitment 之后存储整个 Merkle 树,以便在执行 openings 时,无需重新计算它们。但是,如果这太消耗内存,prover 可能会选择不存储它并在以后重新计算它。CachingCommitmentSchemeProver 实现了这个逻辑。
它有一个内部的 commitment scheme,它将事物分成包并将它们传递给内部的 commitment scheme。
FRI 部分负责生成 FRILayers、生成查询点和生成证明。该证明由来自每一层 Merkle 树的几个元素加上包含证明(身份验证路径)组成。
Frifolder 从前一层中获取两个 evaluations,并使用 FriFolderBase
类计算当前层的 evaluation。FRI 协议允许提交到某些层的子组(例如,每隔一层)。可以改变提交的层数,但这会使逻辑更加复杂。建议在此 issue 中提交每三层。但是,FRI 步长向量使新用户更难使用 prover,并且我们不认为它在性能方面特别有优势。
该协议区分数据查询和完整性查询;如果 evaluation 是完整性查询的一部分,则它不作为证明的一部分提供。这是因为完整性查询可以从前几层的元素中推断出来。我们不需要直接检查该值;如果我们正确计算了该值,则包含证明应该通过。更具体地说,如果 prover 发送与 pk(xi)pk(xi) 和 pk(−xi)pk(−xi) 对应的值,则 verifier 可以计算 pk+1(x2i)pk+1(xi2)。需要此值来检查 Merkle 树中的包含证明;如果我们使用错误的值,则验证应该失败(除非哈希函数发生冲突)。
该协议在 proof 大小方面进行了优化,因为它避免了从完整性查询发送不必要的信息,成对的值分组在 Merkle 树的同一分支中,并且该协议在达到零度之前完成。
在这篇文章中,我们介绍了 Stone prover 的不同组件、它们的工作原理以及它们对 proof 大小的一些影响。Starkware 在开发 prover 和开源方面做得非常出色。仍然有一些部分需要改进,但这在软件中总是如此。
我们目前正在努力实现 Stone 和 STARK Platinum 之间的兼容性。为了实现这个目标,我们需要调整不同的部分,以便我们生成的 challenges 相同,并且我们得到的 proof(从采样查询中)及其序列化和反序列化也完全相同。我们将继续解释 Stone Prover 的工作原理以及我们添加到 STARK Platinum 中的优化,以提高其性能,同时保持兼容性。
我们采访了正在开发 Iroh 的团队,Iroh 是一个可以正常工作的 Rust 对等库。
- 原文链接: blog.lambdaclass.com/ove...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!