Polygon zkEVM采用状态机模型来模拟EVM(Ethereum Virtual Machine),从而提供与以太坊相同的用户体验,并支持部署运行相同的以太坊智能合约。
Polygon zkEVM zkRollup扩容策略在于:
状态机最适合迭代确定性计算,确定性计算在以太坊中很常见。而算术电路将需要展开loop,从而导致不希望的更大的电路。
基于状态机所实现的zkProver证明系统基本设计原则为:
以上前四步通常称为Arithmetization:
同时,由于zkProver部署的上下文为某公钥密码学系统,还需要一种承诺方案。zkProver证明计算正确性并允许任何独立方验证validity proof能力的基础是多项式承诺方案。
Storage状态机为zkProver的二级状态机之一,负责存储于zkProver storage的所有数据操作:
Polygon zkEVM的Storage状态机类似于一个微处理器,具有:
为了使Storage状态机执行Storage Actions,其Executor生成committed多项式和constant多项式,然后根据多项式恒等式对其进行检查,以证明计算是正确执行的。
小结:
前序博客有:
为实现zero-knowledge,所有的数据都存储于Merkle tree中,即意味着Storage SM(State Machine,状态机)需常向其它状态机——Poseidon SM,发送请求(POSEIDON Actions),来执行哈希运算。
key-value数据存储在SMT(Sparse Merkle Tree)中,主状态机基于这些存储在SMT中的key-value数据进行计算。key和value均以256bit string表示,也可解析为256-bit unsigned integers。
zkProver的SMT为Merkle Tree 与 Patricia Tree的结合。
以8-bit key length为例。NULL SMT或 empty SMT 的root为0,即在其中未记录任何key或value。zero node或NULL node,意味着该node中国无任何value。key会决定binary SMT的形状。【所谓binary,是指其path上的label只能为0或1。】
在binary SMT中,其branches要么为leaf,要么为zero-node。
如只有一个key-value pair ( K a , V a ) (K_a,V_a) (Ka,Va)的binary tree,从 K a K_a Ka最低有效位起,逐bit代表从root到leaf的分支选择,0表示左侧,1表示右侧。若 K a = 11010110 K_a=11010110 Ka=11010110,对应的binary SMT为:【最低有效位为0,置于左侧,zero-node置于右侧, L a = H ( V a ) L_a=H(V_a) La=H(Va)。若最低有效位为1,则置于右侧,不过 ( r o o t a 0 = H ( L a ∣ ∣ 0 ) ) ≠ ( r o o t 0 a = H ( 0 ∣ ∣ L a ) ) (root_{a0}=H(L_a||0))\neq (root_{0a}=H(0||L_a)) (roota0=H(La∣∣0))=(root0a=H(0∣∣La)),分别表示的是不同的binary tree。】
若binary SMT中具有2个key-value pair,则,要根据其自最低有效位起,哪个bit不同来摆放。如最低2个有效位都相同,第三个才不同,则摆放情况类似为:
leaf level:表示某leaf到root的深度。如上图: lvl ( L a ) = 3 \text{lvl}(L_a)=3 lvl(La)=3。SMT中最大的leaf level决定了SMT的height。
SMT中所有的key具有相同的固定的key-length,SMT的最大height为该fixed key-length。
不同于通用SMT之处:
假设某SMT由7个key-value pair,相应的key分别为:
K a = 10101100 , K b = 10010010 , K c = 10001010 , K d = 11100110 , K e = 11110101 , K f = 10001011 , K g = 00011111 K_a =10101100,K_b =10010010,K_c =10001010,K_d =11100110,K_e =11110101,K_f=10001011,K_g =00011111 Ka=10101100,Kb=10010010,Kc=10001010,Kd=11100110,Ke=11110101,Kf=10001011,Kg=00011111
以上图7 leaf SMT为例,相应的Remaining key分别为:
R K a = 101011 , R K b = 1001 , R K c = 1000 , R K d = 11100 , R K e = 111101 , R K f = 10001 , R K g = 00011 RK_a =101011,RK_b =1001,RK_c =1000,RK_d =11100,RK_e =111101,RK_f=10001,RK_g =00011 RKa=101011,RKb=1001,RKc=1000,RKd=11100,RKe=111101,RKf=10001,RKg=00011
至此,由于SMT中的leaf具有不同的深度,leaf和branch node采用相同的哈希函数,会存在fake-leaf攻击问题:
解决方案为,leaf和branch node采用不同的哈希函数,进行区分,使得:
B a b = H noleaf ( L a ∣ ∣ L b ) ≠ H leaf ( L a ∣ ∣ L b ) = L ~ f k B_{ab}=H_{\text{noleaf}}(L_a||L_b)\neq H_{\text{leaf}}(L_a||L_b)=\tilde{L}_{fk} Bab=Hnoleaf(La∣∣Lb)=Hleaf(La∣∣Lb)=L~fk
从而,在未知 L a , V a , L b , V b L_a,V_a,L_b,V_b La,Va,Lb,Vb的情况下,无法以 L f k , V f k L_{fk},V_{fk} Lfk,Vfk来欺骗Verifier。
至此,若SMT中有leaf ( K d , V d ) (K_d,V_d) (Kd,Vd),Malicous prover可以SMT中不存在的leaf ( K x , V x ) (K_x,V_x) (Kx,Vx),其中 V x = V d V_x=V_d Vx=Vd,让Verifier误以为 ( K x , V x ) (K_x,V_x) (Kx,Vx)存在与SMT中:
引起该问题的根本原因在于,key-value pair中的key与value未实现binding。相应的解决方案有:
方案2)更优的原因在于,Verifier在验证merkle proof时,仅需要知悉更短的remaining key。
至此,key pair ( K x , V x ) (K_x,V_x) (Kx,Vx)的leaf L x L_x Lx表示为:
L x = H leaf ( R K x ∣ ∣ V x ) L_x=H_{\text{leaf}}(RK_x||V_x) Lx=Hleaf(RKx∣∣Vx)
将value V x V_x Vx以明文存储在leaf L x L_x Lx中,Verifier验证Merkle proof时需知道相应的value值,为实现zero-knowledge属性,可改为在leaf中存储的是 V x V_x Vx的哈希值(采用不同于leaf的哈希函数 H noleaf H_{\text{noleaf}} Hnoleaf):
Hashed Value = HV x = H noleaf ( V x ) \text{Hashed Value}=\text{HV}_x=H_{\text{noleaf}}(V_x) Hashed Value=HVx=Hnoleaf(Vx)【从而将value值 V x V_x Vx隐藏在了 HV x \text{HV}_x HVx中了。】
L x = H leaf ( R K x ∣ ∣ HV x ) L_x=H_{\text{leaf}}(RK_x||\text{HV}_x) Lx=Hleaf(RKx∣∣HVx)
Storage状态机执行的操作称为Storage Actions,Storage状态机负责验证 主状态机所执行的增删改查操作 是否正确。
Prover:
Prover需给Verifier提供,即,Verifier需要知道:
READ ( K x ) \text{READ}(K_x) READ(Kx)操作有2种结果:【目的是证明相应的 K x K_x Kx在SMT中未设置】
UPDATE操作不会改变SMT树的形状。因此,在执行UPDATE时,保留节点的所有labels是很重要的。
UPDATE(key)
操作的基本流程为:
READ(key)
操作,检查相应的leaf存在于old_root对应的SMT树中。仅当本环节验证通过后才进入下一环节。CREATE操作会向SMT中插入新 leaf L n e w L_{new} Lnew,并在 leaf L n e w L_{new} Lnew中存储新的 key-value pair ( K n e w , V n e w ) (K_{new}, V_{new}) (Knew,Vnew),要求:
实际CREATE操作有2种情况:
DELETE操作是指从binary SMT中移除某特定的key-value pair,为CREATE的反向操作。
DELETE操作有2种情况:
1)等价为将某non-zero leaf UPDATE 为 NULL leaf。此时SMT的形状不会改变。当所删除的leaf具有non-zero sibling-node时,对应此场景。
2)等同于CREATE的反向操作。需要从树中移除extension branches,数的形状会改变。当所删除的leaf具有zero sibling-node时,对应此场景。
在Storage状态机中,所有的key和value均为256 bits string:
在Storage状态机中设计的key-value pair SMT上下文中,key可唯一标识leaf,leaf的values会变化,但key不变。
因此,key必须确定性的生成,且不存在碰撞问题。key与leaf之间存在一一对应关系。采用抗碰撞哈希函数来生成key,是个不错的选择。用于生成key的哈希函数参数有:
为此,引入了POSEIDON哈希函数来生成key。【实际上,key=POSEIDON_HASH(account_address, storage_slot, query_key)
。】
由于Storage SMT中的key采用4个64-bit field elements表示,因此,其path为自root到指定leaf,分别取各个field element的最低有效位、次低有效位等等来组成path:
当做UPDATE等操作时,需要根据remaining key和path-bits重构出完整的key,这实际为 2.3.2节的反向操作。
实际实现时,为避免做modulo 4
运算(因将key以4个元素来表示),引入了 1个寄存器 和 1个操作:
LEVEL
寄存器:由4个bits组成,其中3个bit为0,1个bit为1。LEVEL
寄存器的初始值为 ( 1 , 0 , 0 , 0 ) (1,0,0,0) (1,0,0,0)。ROTATE_LEVEL
opcode:每次对 LEVEL
寄存器进行左移1位的rotation。【当每次Prover需要climb the tree时,会使用ROTATE_LEVEL
opcode。】ROTATE_LEVEL
操作,结果保持不变:Storage状态机设计为微处理器,由3部分组成:
Storage Assembly code 为 主状态机 与 Storage Executor之间的 interpreter:
Storage状态机中具有primary Storage Assembly code,来将 主状态机的指令,映射为,对应每个基本操作的secondary Assembly code。
这些基本操作主要为本文之前提到的CREATE\READ\UPDATE\DELETE操作。
Storage状态机中主要有8种secondary Assembly code,详情见:https://github.com/0xPolygonHermez/zkevm-storage-rom/tree/main/zkasm,具体的映射关系为:
Storage Actions | File Names | Code Names | Action Selectors In Primary zkASM Code |
---|---|---|---|
READ | Get | Get | isGet() |
UPDATE | Set_Update | SU | isSetUpdate() |
CREATE new value at a found leaf | Set_InsertFound | SIF | isSetInsertFound() |
CREATE new value at a zero node | Set_InsertNotFound | SINF | isSetInsertNotFound() |
DELETE last non-zero node | Set_DeleteLast | SDL | isSetDeleteLast() |
DELETE leaf with non-zero sibling | Set_DeleteFound | SDF | isSetDeleteFound() |
DELETE leaf with zero sibling | Set_DeleteNotFound | SDNF | isSetDeleteNotFound() |
SET a zero node to zero | Set_ZeroToZero | SZTZ | isSetZeroToZero() |
Storage状态机的输入和输出状态均为SMT,形如:
不过状态机中采用的为寄存器,而不是变量。基本操作所需的所有值,均存储在primary Assembly code的如下寄存器中:
HASH_LEFT
, HASH_RIGHT
, OLD_ROOT
, NEW_ROOT
, VALUE_LOW
, VALUE_HIGH
, SIBLING_VALUE_HASH
, RKEY
, SIBLING_RKEY
, RKEY_BIT
, LEVEL
.其中SIBLING_VALUE_HASH
和SIBLING_RKEY
这2个寄存器仅由 Set_InsertFound
和Set_DeleteFound
这2个secondary Assembly code使用。其余的寄存器则被所有secondary Assembly code使用。
primary Assembly code 借助 selectors 来将 主状态机指令 转换为 相应的Storage Actions。selectors要么为0,要么为1:
Storage Executor类似于a slave-worker to the master,此处master,是指the Storage Assembly code。Executor根据Assembly code中所定义的规则和逻辑来执行所有的Storage Actions。
对于 主状态机中的每个指令,借助之前提到的secondary Assembly code selectors,Storage Executor会调用 以JSON文件存储的Storage ROM 中的特定secondary Assembly code函数。相应的函数有:
GetSibling()
, GetValueLow()
, GetValueHigh()
, GetRKey()
, GetSiblingRKey()
, GetSiblingHash()
, GetSiblingValueLow()
, GetSiblingValueHigh()
, GetOldValueLow()
, GetOldValueHigh()
, GetLevelBit()
, GetTopTree()
, GetTopBranch()
以及 GetNextKeyBit()
Storage中所执行的所有计算都必须是可verifiable的,因此引入了PIL code来建立Verifier所需的所有多项式约束,以验证执行的正确性。
这些多项式约束是自Storage Executor开始准备的。为此,Storage Executor使用了:
这些均为Boolean多项式,具体的Boolean committed多项式有:
Selectors | Setters | Instructions |
---|---|---|
selFree[i] | setHashLeft[i] | iHash |
selSiblingValueHash[i] | setHashRight[i] | iHashType |
selOldRoot[i] | setOldRoot[i] | iLatchSet |
selNewRoot[i] | setNewRoot[i] | iLatchGet |
selValueLow[i] | setValueLow[i] | iClimbRkey |
selValueHigh[i] | setValueHigh[i] | iClimbSiblingRkey |
selRkeyBit[i] | setSiblingValueLow[i] | iClimbSiblngRkeyN |
selSiblingRkey[i] | setSiblingValueHigh[i] | iRotateLevel |
selRkey[i] | setRkey[i] | iJmpz |
setSiblingRkey[i] | iConst0 | |
setRkeyBit[i] | iConst1 | |
setLevel[i] | iConst2 | |
iConst3 | ||
iAddress |
每次使用或执行这些Boolean多项式时,都会在其寄存器中记录“1”,也称为Execution Trace。
因此,无需执行某些昂贵的运算来验证执行的正确性,仅对execution trace进行验证即可。
Verifier可获得该execution trace,验证其满足PIL code中的多项式约束(或 多项式identities)。该技术有助于zkProver实现succinctness ZKP。
[1] zkProver Design Approach
[2] Storage SM