美文网首页
Polygon zkEVM zkProver设计

Polygon zkEVM zkProver设计

作者: 雪落无留痕 | 来源:发表于2022-10-06 00:31 被阅读0次

    zkProver主要完成批量交易的证明,是Polygon zkEVM的关键组件。下图展示了zkProver和Node, Database交互的过程。

    1. Node 会发送以太坊状态内容和EVM Merkle 树到DB 中,存储起来;
    2. Node 将批量交易作为zkProver的输入;
    3. zkProver 访问DB, 获取用于生成交易可验证证明数据;
    4. zkProver 生成交易的证明,然后返回给Node.

    zkProver 概览

    zkProver主要有几个组件:

    • Executor: 主要负责主状态机执行;
    • STARK 递归组件:生成STARK 证明;
    • CIRCOM 库: 主要成生成STARK验证电路;
    • zk-SNARK Prover: 主要生成验证STARK 的SNARK 证明。

    zkProver 数据处理流程

    Executor

    Excutor 是主状态机的执行者,它的输入包括: 交易,旧的状态根,新的状态根,ChainID等。另外,executor 需要zkEVM当前状态的值用以构建证明。

    Executor 还需要:

    • ROM: 存储了执行的指令;
    • PIL: 一系列寄存器的多项式,定义了它们的约束关系;

    Executor 实现是汇编语言(zkASM)的解释器,zkASM 用来构建称为zkEVM-ROM的程序,然后由Executor 执行生成合适的执行路径。 在zkEVM-ROM程序中,每个EVM opcode 通过一系统zkASM 指令实现。每个指令使用执行路径矩阵的一行,也称为zkEVM的一步。

    STARK 递归组件

    Executor 执行完成后,会生成一系列多项式,STARK递归组件将其作为输入,主要包含:

    1. 承诺多项式;
    2. 常量多项式;
    3. 一系列指令的脚本;

    STARK递归组件采用FRI (Fast Reed-Solomon Interactive Oracle Proof of Proximity, FRS-IOPP) 生成zk-STARK 证明。

    采用递归的原因为:

    • 实际上是生成多个zk-STARK 证明;
    • 将其分成多组 zk-STARK 证明;
    • 对于每个组,合并成一个zk-STARK 证明;
    • 对于所有的组,最终合并成一个zk-STARK 证明。

    因此,最终将几百个zk-STARK 证明 合并成一个zk-STARK 证明。

    CIRCOM 库

    CIRCOM 组件将生成的zk-STARK 证明作为输入,生成witness, 以R1CS(Rank-1 Constraints System)形式表示约束。

    zk-SNARK Prover

    zk-SNARK Prover 采用Rapid SNARK方案,根据CIRMCOM生成的约束系统,最终生成zk-SNARK 证明。

    zk-STARK 具有较快的证明者速度,不需要可信设置,但是生成的证明比较大;采用zk-SNRK来验证zk-STARK证明,可以大幅减少证明大小,大幅减少gas花费(从 5M 到 350 K), 实现简洁性目标。

    两种新的语言

    Polygon Hermez 创建了两种新的语言: zkASM(Zero-knowledge Assembly) 和 PIL (Polynomial Identities Language) 。

    • zkASM: 主要将主状态机的指令映射到其它状态机,作为固件的解释器,由Executor 执行,生成执行路径;
    • PIL: 将计算转变为多项式约束关系,描述这些多项式的恒等关系,然后用于生成证明。

    zkASM

    zkASM(zero-knowledge Assembly) 以更加抽象的方式,描述处理器的ROM, 告诉Executor 如何解释输入的各种交易,然后由Executor 生成多项式。

    基本语法

    zkASM 的每个指令顺序执行(除了jump), 指令被一行一行地描述。

    注释和模块

    注释用 ";" 表示, 目前只支持单行注释:

    ; This a totally useful comment
    

    zkASM 代码划分为多个文件,通过INCLUDE 导入,实现zkASM 的模块化:

    ; File: main.zkasm
    
    INCLUDE "utils.zkasm"
    INCLUDE "constants.zkasm"
    ; -- code --
    

    寄存器赋值

    有多种寄存器赋值方式:

    1. 采用 "=>" 将常量赋给1个或多个寄存器:

      0 => A, B
      
    2. 将一个寄存器值赋给其它寄存器:

    A => B, C
    

    也可以将函数值存在寄存器中:

    f(A,B) => C,D
    
    1. 将全局变量值存入寄存器:

      %GLOBAL_VAR => A,B
      
    2. executor 执行结果存入1个或多个寄存器中,使用 $ 符号。

    ${ExecutorMethod(params)} => A,B
    
    1. 若一个方法独自执行,主要是用来生成日志信息:
    ${ExecutorMethod(params)}
    
    1. 除了executor 方法,还可以使用内联函数.
    ${A >> 2} => B
    ${A & 0x03} => C
    

    引入操作码

    之前每个指令主要和寄存器交互,下面将和ROM的其它部分交互。

    使用下面的语法将zkEVM的操作码存入某些寄存器中:

    $ => A,B    :OPCODE(param)
    

    其中一个简单的例子是从内存中载入操作码:

    $ => A,B    :MLOAD(param)
    

    当寄存器出现在操作码的一边,表示寄存器A 是内存存储操作码的输入。

    A   :MSTORE(param)
    

    类似地,可以将自由输入存到寄存器,然后执行一些zkEVM 操作码:

    ${ExecutorMethod(params)} => A      :OPCODE1
                                        :OPCODE2
                                        :OPCODE3
                                        ...
    
    

    executor 方法和一个寄存器存储结果,可以和 jump 操作码结合,用来处理一些意外的情形,例如gas 不足:

    ${ExecutorMethod(params)} => A :JMP(param)
    

    有时也会遇到反方向的跳转,用来执行即将发生的操作:

    SP - 2  :JMPN(stackUnderflow)
    

    代码注入

    内联javascript 指令可以用 $$ 表示:

    $${CODE}
    

    断言

    下面的断言比较寄存 B 和寄存器A 中的值。

    B    :ASSERT
    

    示例

    下面将EVM ADD 作为第一个介绍性的示例:

    opADD:
        SP - 2          :JMPN(stackUnderflow)
        SP - 1 => SP
        $ => A          :MLOAD(SP--)
        $ => C          :MLOAD(SP)
    
        ; Add operation with Arith
        A               :MSTORE(arithA)
        C               :MSTORE(arithB)
                        :CALL(addARITH)
        $ => E          :MLOAD(arithRes1)
        E               :MSTORE(SP++)
        1024 - SP       :JMPN(stackOverflow)
        GAS-3 => GAS    :JMPN(outOfGas)
                        :JMP(readCode)
    
    1. 首先检查stack 是否适合 ADD 操作。 ADD 操作码两个元素,它会检查这两个元素确实在stack 上。

      SP - 2          :JMPN(stackUnderflow)
      

    若少于两个元素,则 stackUnderflow 函数执行。

    1. 然后分别移动stack 指针,分别将值存入 寄存器 AC中:
    SP - 1 => SP
    $ => A          :MLOAD(SP--)
    $ => C          :MLOAD(SP)
    
    1. 分别将寄存器 AC 存入变量 arithAarithB 中,然后调用addARITH , 执行实际的加法运算:
    A               :MSTORE(arithA)
    C               :MSTORE(arithB)
                    :CALL(addARITH)
    $ => E          :MLOAD(arithRes1)
    E               :MSTORE(SP++)
    

    最后,将加法结果移入寄存器 E 中,将对应的值放到stack指针位置。

    1. 执行多个检查,如stack 没有满,gas 未耗尽。
    1024 - SP       :JMPN(stackOverflow)
    GAS-3 => GAS    :JMPN(outOfGas)
                    :JMP(readCode)
    

    最后,还有一个指令用来指示移动下一个指令中。

    PIL

    PIL (Polynomial Identity Language) 是一种用来定义状态机的域相关的语言。创建PIL 的目的是为开发者提供一种构建状态机的友好框架,将证明机制的复杂性抽象出来。PIL 的一个主要特别是模块化,允许开发者定义参数化的状态机:namespaces。 通过构建模块化状态机,更容易测试、审计和形式化验证复杂的状态机。PIL 的关键特点有:

    • 提供 \mathtt{namespaces},对状态机必要的部分进行命名;
    • 表示多项式是承诺的,还是不变的;
    • 表达多项式的有关系,可能是恒等,或 \mathtt{lookup arguments};
    • 指定多项式的类型,例如 bool 或 u32.

    Hello World 示例

    对于一个状态机,输入为两个数 xy, 作乘法运算,也称为 Multiplier, 通过下面的函数表示:
    f(x, y) = x \cdot y
    下表展示了不同输入的计算路径,有两个自由输入多项式 \mathtt{ freeIn_1}\mathtt{freeIn_2}, 输出多项式为\mathtt{out}, 为输入多项式的乘积。

    由此可以将多项式分为两类:

    • \textbf{Free Input Polynomials}: 主要用来在计算中引入更多输入, 值不依赖之前的输入;
    • \textbf{State Variables}: 组成状态机的状态,代表状态机输出的每一步,若是最后一步,则表示整个计算的输出。

    Multiplier 状态机的约束条件为:
    out = freeIn_1 \cdot freeIn_2

    PIL 组件

    本节深入讲解PIL 的组件。

    Namespaces

    [图片上传失败...(image-f2d442-1664987363581)]

    状态机在PIL以namespaces方式组织,用作一个命名空间,将状态机所有的多项式和恒等关系包含在内,也包含了状态机的组件。命名空间不允许重名。

    Polynomials

    [图片上传失败...(image-7dbcd2-1664987363581)]

    常量多项式,是预处理的多项式,在状态机执行前就已经确定,并且在执行过程中保持不变,例如selectorslatches, sequencers

    承诺多项式只有证明者知道,在执行过程中会改变。承诺多项式分两类:输入多项式和状态变量。

    Free Input Polynomials: 用来引入状态机的输入数据。

    State Variables: 状态变量可以认为是状态机的的状态。

    多项式元素类型

    [图片上传失败...(image-1963c3-1664987363582)]

    可以通过关键字表明多项式类型,例如 bool, u16, field, 以实现元素域的约束条件。

    约束是PIL 代码最关键的部分,用来定义多项式之间的关系,实现条件约束。

    [图片上传失败...(image-f498eb-1664987363582)]

    约束条件一般有以下几种类型:

    <img src="https://docs.hermez.io/zkEVM/PIL/figures/fig19-pol-constrnts-four.png" alt="The Main Polynomial Constraints" style="zoom: 50%;" />

                          Main Polynomial Constraints
    

    循环结构

    状态机需要有循环性质,尤其是从最后一行到第一行的状态转换,在设计状态机约束条件时候需要特别注意。

    [图片上传失败...(image-bf1aec-1664987363582)]

                                                                          状态机循环性质
    

    若最后一次状态转换约束未满足,可以通过人工添加值的方式解决。

    使如对于如下的状态机和对应的计算路径:

    [图片上传失败...(image-b1981f-1664987363582)]

    明显, 约束条件 b'=b+a在最后一个转换中不满足:(a,b)=(1,2)(a,b)=(1,1)

    可以通过添加额外两行方式解决,即:

    <img src="https://docs.hermez.io/zkEVM/PIL/figures/fig11-indc-cycl-ntr.png" alt="Inducing a Cyclic Nature" style="zoom:33%;" />

    另外一 种办法是引入选择子,即:

    [图片上传失败...(image-89eab7-1664987363582)]

    第三种办法是利用现有的选择子进行处理。

    模块化

    虽然一些多项式可以添加到状态机中用以表述更多操作,但这种设计很难测试,审计和验证。

    为了避免这些复杂性,PIL 采取分而治之的策略:

    • 不是仅开发一个大的状态机,而是采用由不同状态机组成的架构;
    • 每个状态机证明一项指定的任务,有一系列约束条件;
    • 不同状态机的相关多项式通过lookup tablespermutation arguments 关联在一起;
    • 保证一致性,就像仅有一个单一的状态机。

    PIL 因此非常适合状态机的模块化设计。下图描述了多项式[a,b,c]和[d,e,f]之间的连接关系。

    <img src="https://docs.hermez.io/zkEVM/PIL/figures/fig12-pol-cnnct-sms.png" alt="Polynomial Connections Across State Machines" style="zoom:33%;" />

                                                   跨状态机之间的多项式连接
    

    为了说明这个过程:

    • 首先设计一个状态机,管理2字节元素的算术化操作;
    • 通过lookup argument 连接两个状态机。

    算术状态机

    算术状态机用来检查一些算术操作,例如加法和乘法, 在2字节元素上正确执行,对于多项式\mathtt{a,b,c,d,e}, 需要满足下面的恒等关系:
    a(x)\cdot b(x)+c(x)=2^{16}\cdot d(x)+e(x)
    需要注意的是:

    (a) ab 之间乘法操作, 可以通过 ed 描述,它们都是2字节的元素;

    (b) 约束 a, b,c,d,e 的值都是2字节的值。

    <img src="https://docs.hermez.io/zkEVM/PIL/figures/fig13-arth-sm-arch.png" alt="Architecture of the Arithmetic State Machine" style="zoom:33%;" />

                                                                            算术状态机的架构
    

    上图展示了算术状态机的设计架构, 下表显示计算的执行路径:

    [图片上传失败...(image-5633e4-1664987363582)]

                                                           算术状态机的计算路径
    

    算术状态机工作方式如下: \mathtt{LATCH} 来用标记运算是否准备好。 \mathtt{SET[A]}, \mathtt{SET[B]}, \mathtt{SET[C]}, \mathtt{SET[E]}\mathtt{LATCH} 为常量多项式,\mathtt{freeIn}为承诺多项式,a,b,c,d,e 为状态变量,

    算术状态机多项式的约束为:
    \begin{aligned} &\texttt{freeIn} \subset [0,2^{16} - 1], \\ \texttt{a}' &= \texttt{SET}[A]\cdot(\texttt{freeIn} - \texttt{a}) + \texttt{a}, \\ \texttt{b}' &= \texttt{SET}[B]\cdot(\texttt{freeIn} - \texttt{b}) + \texttt{b}, \\ \texttt{c}' &= \texttt{SET}[C]\cdot(\texttt{freeIn} - \texttt{c}) + \texttt{c}, \\ \texttt{d}' &= \texttt{SET}[D]\cdot(\texttt{freeIn} - \texttt{d}) + \texttt{d}, \\ \texttt{e}' &= \texttt{SET}[E]\cdot(\texttt{freeIn} - \texttt{e}) + \texttt{e}, \\ 0 &= [ \texttt{a} \cdot \texttt{b} + \texttt{c} - (2^{16} \cdot \texttt{d} + \texttt{e}) ] \cdot \texttt{LATCH}. \end{aligned}
    在PIL 代码中可以如下描述:

    [图片上传失败...(image-681699-1664987363582)]

    主状态机

    主状态机主要负责主要的一些任务,但会指定使用算术状态机当需要的时候。

    <img src="https://docs.hermez.io/zkEVM/PIL/figures/fig15-main-sm-arch.png" alt="The Main State Machine Architecture" style="zoom:33%;" />

                                                                 主状态机架构
    

    PIL 的第一任务是引入大量的多项式,如下所示:
    [图片上传失败...(image-3ce6aa-1664987363582)]

    现在可以添加主状态机变量a,b,c,d,e的大量约束,下图展示了所需要的行为:

    <img src="https://docs.hermez.io/zkEVM/PIL/figures/fig16-main-sm-bool-pols.png" alt="Boolean Polynommials in the Main State Machine" style="zoom:33%;" />

    在PIL中,表示如下:

    [图片上传失败...(image-22f9e6-1664987363582)]

    最后,主状态机和算术状态机之间的约束关系可以用下面的约束表示:

    [图片上传失败...(image-f16ff3-1664987363582)]

    这种连接关系可以下面的表描述,即:

    [图片上传失败...(image-def9bc-1664987363582)]

    一方面,\mathtt{arith} 选择子用来标记主状态机中的在算术状态机中的lookup, 另一方面,\mathtt{LATCH} 也是一个选择子,哪些行需要添加到lookup证明,即最后需要证明:
    \begin{array}{c} \texttt{Main.arith} \cdot [\texttt{Main.a} , \texttt{Main.b} , \texttt{Main.c} , \texttt{Main.d}, \texttt{Main.e}] \\ \subset \\ \texttt{Arith.LATCH} \cdot [\texttt{Arith.a}, \texttt{Arith.b}, \texttt{Arith.c}, \texttt{Arith.d}, \texttt{Arith.e}]. \end{array}

    高级特性

    公开输入

    公开输入是多项式的值,在状态机执行前已知。在下面的示例中,\mathtt{publicInput} 被设为多项式 a 的第一个值,分号 ":" 用来向编译器注明:

    [图片上传失败...(image-906e91-1664987363582)]

    这里使用Lagrage 多项式 L_1 用来创建一个约束条件:
    L_1 \cdot (\texttt{a} - :\texttt{publicInput}) = 0.
    Permutation check

    下面的例子中,\mathtt{is} 关键字用来表示 [\mathtt{sm1.a}, \mathtt{sm1.b},\mathtt{sm1.c}]\mathtt{sm2.a}, \mathtt{sm2.b}, \mathtt{sm2.c} 是置换关系,

    [图片上传失败...(image-c62cd3-1664987363582)]

    这个约束可以用连接不同的状态机,约束不同状态机的多项式是相同的。

    两个特殊的功能

    \mathtt{in}\mathtt{is} 主要用于以下设计:
    \begin{array}{ccc} (3,2) & \text{in} & (1,2,3,4)\\ (1,5,5,5,8,1,1,2) & \text{in} & (1,2,4,5,8)\\ (3,2,3,1) & \text{is} & (1,2,3,3)\\ (5,5,6,9,0) & \text{is} & (6,5,9,0,5). \end{array}
    Connect关键字

    connect 关键字用来表示复制约束证明,下例中,[a,b,c] 用来表示 [SA,SB,SC]的置换。

    [图片上传失败...(image-3131bc-1664987363582)]

    先前的一些特性可以用来描述整个Plonk电路的正确性。

    [图片上传失败...(image-ea1ff8-1664987363582)]

    Permutaion Check with Multiple Domain

    另外一个重要特性是证明不同状态机子集元素的置换关系。PIL 通过引入 selectors,选择包含进置换证明的子集元素。

    [图片上传失败...(image-31d439-1664987363582)]

    参考

    https://polygon.technology/solutions/polygon-zkevm

    https://docs.hermez.io/

    https://github.com/0xPolygonHermez/

    https://github.com/socathie/circomlib-ml

    https://www.techrxiv.org/articles/preprint/CIRCOM_A_Robust_and_Scalable_Language_for_Building_Complex_Zero-Knowledge_Circuits/19374986/1

    相关文章

      网友评论

          本文标题:Polygon zkEVM zkProver设计

          本文链接:https://www.haomeiwen.com/subject/ggfxnrtx.html