appliedzkp的zkevmState Circuit
Posted mutourend
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了appliedzkp的zkevmState Circuit相关的知识,希望对你有一定的参考价值。
1. 引言
State circuit的作用是,作为:random accessible data holder of stack, memory, storage, and all the other things evm interpreter could access at any time。
State circuit可分为3个子circuit:
- stack sub-circuit
- memory sub-circuit
- storage sub-circuit
以如下solidity代码为例:
pragma solidity ^0.8;
contract Sample {
function memory_sample() public pure {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0xdeadbeaf)
mstore(ptr, add(mload(ptr), 0xfaceb00c))
mstore(add(ptr, 0x20), 0xcafeb0ba)
}
}
}
当执行memory_sample
函数时,evm中会有如下log:
pc op stack (top -> down) memory
-- -------------- ---------------------------------- ---------------------------------------
...
53 JUMPDEST [ , , , ] {40: 80, 80: , a0: }
54 PUSH1 40 [ , , , 40] {40: 80, 80: , a0: }
56 MLOAD [ , , , 80] {40: 80, 80: , a0: }
57 PUSH4 deadbeaf [ , , deadbeef, 80] {40: 80, 80: , a0: }
62 DUP2 [ , 80, deadbeef, 80] {40: 80, 80: , a0: }
63 MSTORE [ , , , 80] {40: 80, 80: deadbeef, a0: }
64 PUSH4 faceb00c [ , , faceb00c, 80] {40: 80, 80: deadbeef, a0: }
69 DUP2 [ , 80, faceb00c, 80] {40: 80, 80: deadbeef, a0: }
70 MLOAD [ , deadbeef, faceb00c, 80] {40: 80, 80: deadbeef, a0: }
71 ADD [ , , 1d97c6efb, 80] {40: 80, 80: deadbeef, a0: }
72 DUP2 [ , 80, 1d97c6efb, 80] {40: 80, 80: deadbeef, a0: }
73 MSTORE [ , , , 80] {40: 80, 80: 1d97c6efb, a0: }
74 PUSH4 cafeb0ba [ , , cafeb0ba, 80] {40: 80, 80: 1d97c6efb, a0: }
79 PUSH1 20 [ , 20, cafeb0ba, 80] {40: 80, 80: 1d97c6efb, a0: }
81 DUP3 [ 80, 20, cafeb0ba, 80] {40: 80, 80: 1d97c6efb, a0: }
82 ADD [ , a0, cafeb0ba, 80] {40: 80, 80: 1d97c6efb, a0: }
83 MSTORE [ , , , 80] {40: 80, 80: 1d97c6efb, a0: cafeb0ba}
84 POP [ , , , ] {40: 80, 80: 1d97c6efb, a0: cafeb0ba}
...
相关定义有:
- fq:253-bit value
- op:表示EVM operation code
- pc:Program counter
- gc:Global counter,简化为offset to 0
- sp:Stack pointer
- stack:为fq 向量,max size为1024
- memory:为a vector of bytes
- $a == $b:表示 $a 等于 $b
- $t_lookup:为用于保证input在 $t 表中的函数
2. memory circuit
在memory circuit中,Prover应收集所有的MLOAD
和MSTORE
操作,然后根据key
和gc
排序,然后构建a layout with column key
, val
, rw
和 gc
,分别表示:
- key:为 key of memory we are operating
- val:为操作后的 memory[key] 值
- rw:为access enum,如0表示Read,1表示Write
辅助的信息有:
- key_prev:为前一行的key value
- gc_prev:为前一行的gc value
- val_prev:为钱一样的 val value
memory circuit中的constraint为:
以上solidity例子对应的memory table为:【根据 以太坊黄皮书 可知,MLOAD对应为1 memory read + 1 stack read + 1 stack write。DUP 对应为1 stack read + 1 stack write。MSTORE对应2 stack read + 1 memory write。PUSH对应 1 stack write。SWAP对应 2 stack read + 2 stack write。】
3. Stack Circuit
Stack circuit与memory circuit类似,但是为了防止在PUSH或POP时对每个entry的修改,设置在evm circuit中维护一个stack pointer sp,sp的初始位置为1024 to look up the top value of stack。如,PUSH时
s
p
−
−
sp--
sp−−,POP时
s
p
+
+
sp++
sp++。
相应的Stack circuit constraint为:
某些op可同时对应 多个stack read/write,如DUPX,此时需check if the source and new pushed value is equal,因此要求 a Read and a Write to be in bus mapping。
设置evm circuit中使用multiple lookup来保证这些read/write是同时发生的(对于memory也一样),因此需有如下constraints:(其中 $x 表示变量,应与相同的 op 一致。)
以上solidity例子对应的stack table为:
4. Storage circuit
在Storage circuit中,需考虑在sub call中遇到REVERT时的各种应对策略。此处,主要关注:
- storage of account state
- error caused by REVERT
相关定义有:
- gc:global counter
- addr:account address holding opcodes
- pc:program counter
- op:operator respect to addr and pc
- sp:stack pointer
- is_persistant:为a binary flag that tells if this call succeeds in the future (immutable with a call)
- gc_end_of_call:a number for us to know when the call ends, we use this to rollback storage write in FILO order (immutable within a call)
- sstore_counter:为a counter to count how many SSTORE we have done。
在Storage circuit中,有类似evm的层级namespace——group access records by account address and then storage location,然后在每个sub-groups (each location of an account), 按global counter递增对records排序。
(不同于stack和memory,stack和memory可seperated properly by some other unique identical call context instead of only account address for indicating the source of CALLDATA and RETURNDATA。)
同时,为了便于rollback,当 Storage Write时,在access record中额外增加了val_prev。
根据 EIP2929,在bus mapping中额外引入了is_first_touch,当a storage location is touched for the first time in a EOA(externally owned account) call时,该值应设置为1。
最终的bus mapping lookup为:
bus_mapping_lookup(
gc,
Storage,
key,
val,
rw,
val_prev,
is_first_touch,
)
4.1 Multi-Call Example
object "Caller" {
code {
datacopy(0, dataoffset("Callee"), datasize("Callee"))
let callee := create(0, 0, datasize("Callee"))
datacopy(0, dataoffset("runtime"), datasize("runtime"))
setimmutable(0, "callee", callee)
return(0, datasize("runtime"))
}
object "runtime" {
code {
let callee := loadimmutable("callee")
// call 1: success
mstore(0, 1)
pop(call(30000, callee, 0, 0, 32, 0, 0))
// call 2: revert
mstore(0, 0)
pop(call(30000, callee, 0, 0, 32, 0, 0))
// call 3: success
mstore(0, 1)
pop(call(30000, callee, 0, 0, 32, 0, 0))
}
}
object "Callee" {
code {
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// counter++
sstore(0, add(sload(0), 1))
// counter++
sstore(0, add(sload(0), 1))
if calldataload(0) { stop() }
revert(0, 0)
}
}
}
}
在上面的例子中:
- Caller 触发 Callee 做3次简单的storage操作—— Callee.0++,但是其中第2次操作将revert。
calling Caller 的executation trace如下:【为简单起见,将其中的所有DUP替换为了PUSH, ID替换为了CALLIndex,RV替换为了Revert。】
ID RV PC OP STACK STORAGE
---- ---- ----- -------------- ------------------------------ ----------
0 0 0 PUSH1 1 [ , , , , , , 1 ]
0 0 2 PUSH1 0 [ , , , , , 0, 1 ]
0 0 4 MSTORE [ , , , , , , ]
0 0 5 PUSH1 0 [ , , , , , , 0 ]
0 0 7 PUSH1 0 [ , , , , , 0, 0 ]
0 0 9 PUSH1 20 [ , , , , 20, 0, 0 ]
0 0 11 PUSH1 0 [ , , , 0, 20, 0, 0 ]
0 0 13 PUSH1 0 [ , , 0, 0, 20, 0, 0 ]
0 0 15 PUSH20 a [ , a, 0, 0, 20, 0, 0 ]
0 0 36 PUSH2 c350 [ c350, a, 0, 0, 20, 0, 0 ]
0 0 39 CALL [ , , , , , , ]
1 0 0 PUSH1 0 [ , , , , , , 0 ] { 0: 0 }
1 0 2 SLOAD [ , , , , , , 0 ] { 0: 0 }
1 0 3 PUSH1 1 [ , , , , , 1, 0 ] { 0: 0 }
1 0 5 ADD [ , , , , , , 1 ] { 0: 0 }
1 0 6 PUSH1 0 [ , , , , , 0, 1 ] { 0: 0 }
1 0 8 SSTORE [ , , , , , , ] { 0: 1 }
1 0 9 PUSH1 0 [ , , , , , , 0 ] { 0: 1 }
1 0 11 SLOAD [ , , , , , , 1 ] { 0: 1 }
1 0 12 PUSH1 1 [ , , , , , 1, 1 ] { 0: 1 }
1 0 14 ADD [ , , , , , , 2 ] { 0: 1 }
1 0 15 PUSH1 0 [ , , , , , 0, 2 ] { 0: 1 }
1 0 17 SSTORE [ , , , , , , ] { 0: 2 }
1 0 18 PUSH1 0 [ , , , , , , 0 ] { 0: 2 }
1 0 20 CALLDATALOAD [ , , , , , , 1 ] { 0: 2 }
1 0 21 ISZERO [ , , , , , , 0 ] { 0: 2 }
1 0 22 PUSH2 1b [ , , , , , 1b, 0 ] { 0: 2 }
1 0 25 JUMPI [ , , , , , , ] { 0: 2 }
1 0 26 STOP [ , , , , , , 1 ] { 0: 2 }
0 0 40 POP [ , , , , , , ]
0 0 41 PUSH1 0 [ , , , , , , 0 ]
0 0 43 PUSH1 0 [ , , , , , 0, 0 ]
0 0 45 MSTORE [ , , , , , , ]
0 0 46 PUSH1 0 [ , , , , , , 0 ]
0 0 48 PUSH1 0 [ , , , , , 0, 0 ]
0 0 50 PUSH1 20 [ , , , , 20, 0, 0 ]
0 0 52 PUSH1 0 [ , , , 0, 20, 0, 0 ]
0 0 54 PUSH1 0 [ , , 0, 0, 20, 0, 0 ]
0 0 56 PUSH20 a [ , a, 0, 0, 20, 0, 0 ]
0 0 77 PUSH2 c350 [ c350, a, 0, 0, 20, 0, 0 ]
0 0 80 CALL [ , , , , , , ]
2 1 0 PUSH1 0 [ , , , , , , 0 ] { 0: 2 }
2 1 2 SLOAD [ , , , , , , 2 ] { 0: 2 }
2 1 3 PUSH1 1 [ , , , , , 1, 2 ] { 0: 2 }
2 1 5 ADD [ , , , , , , 3 ] { 0: 2 }
2 1 6 PUSH1 0 [ , , , , , 0, 3 ] { 0: 2 }
2 1 8 SSTORE [ , , , , , , ] { 0: 3 }
2 1 9 PUSH1 0 [ , , , , , , 0 ] { 0: 3 }
2 1 11 SLOAD [ , , , , , , 3 ] { 0: 3 }
2 1 12 PUSH1 1 [ , , , , , 1, 3 ] { 0: 3 }
2 1 14 ADD [ , , , , , , 4 ] { 0: 3 }
2 1 15 PUSH1 0 [ , , , , , 0, 4 ] { 0: 3 }
2 1 17 SSTORE [ , , , , , , ] { 0: 4 }
2 1 18 PUSH1 0 [ , , , , , , 0 ] { 0: 4 }
2 1 20 CALLDATALOAD [ , , , , , , 0 ] { 0: 4 }
2 1 21 ISZERO [ , , , , , , 1 ] { 0: 4 }
2 1 22 PUSH2 1b [ , , , , , 1b, 1 ] { 0: 4 }
2 1 25 JUMPI [ , , , , , , ] { 0: 4 }
2 1 27 JUMPDEST [ , , , , , , ] { 0: 4 }
2 1 28 PUSH1 0 [ , , , , , , 0 ] { 0: 4 }
2 1 30 PUSH1 0 [ , , , , , 0, 0 ] { 0: 4 }
2 1 32 REVERT [ , , , , , , 0 ] { 0: 2 }
0 0 81 POP [ , , , , , , ]
0 0 82 PUSH1 1 [ , , , , , , 1 ]
0 0 84 PUSH1 0 [ , , , , , 0, 1 ]
0 0 86 MSTORE [ , , , , , , ]
0 0 87 PUSH1 0 [ , , , , , , 0 ]
0 0 89 PUSH1 0 [ , , , , , 0, 0 ]
0 0 91 PUSH1 20 [ , , , , 20, 0, 0 ]
0 0 93 PUSH1 0 [ , , , 0, 20, 0, 0 ]
0 0 95 PUSH1 0 [ , , 0, 0, 20, 0, 0 ]
0 0 97 PUSH20 a [ , a, 0, 0, 20, 0, 0 ]
0 0 118 PUSH2 c350 [ c350, a, 0, 0, 20, 0, 0 ]
0 0 121 CALL [ , , , , , , ]
3 0 0 PUSH1 0 [ , , , , , , 0 ] { 0: 2 }
3 0 2 SLOAD [ , , , , , , 2 ] { 0: 2 }
3 0 3 PUSH1 1 [ , , , , , 1, 2 ] { 0: 2 }
3 0 5 ADD [ , , , , , , 3 ] { 0: 2 }
3 0 6 PUSH1 0 [ , , , , , 0, 3 ] { 0: 2 }
3 0 8 SSTORE [ , , , , , , ] { 0: 3 }
3 0 9 PUSH1 0 [ , , , , , , 0 ] { 0: 3 }
3 0 11 SLOAD [ , , , , , , 3 ] { 0: 3 }
3 0 12 PUSH1 1 [ , , , , , 1, 3 ] { 0: 3 }
3 0 14 ADD [ , , , , , , 4 ] { 0: 3 }
3 0 15 PUSH1 0 [ , , , , , 0, 4 ] { 0: 3 }
3 0 17 SSTORE [ , , , , , , ] { 0: 4 }
3 0 18 PUSH1 0 [ , , , , , , 0 ] { 0: 4 }
3 0 20 CALLDATALOAD [ , , , , , , 1 ] { 0: 4 }
3 0 21 ISZERO [ , , , , , , 0 ] { 0: 4 }
3 0 22 PUSH2 1b [ , , , , , 1b, 0 ] { 0: 4 }
3 0 25 JUMPI [ , , , , , , ] { 0: 4 }
3 0 26 STOP [ , , , , , , 1 ] { 0: 4 }
0 0 122 POP [ , , , , , , ]
0 0 123 STOP [ , , , , , , ]
对应的storage circuit witnesses为:【简单起见,忽略is_first_touch,因为其仅在第一行为1。】
可发现,在ID=2时,the storage writes are undone in a FILO (first in last out) order。在evm circuit中,将constraint the number of SSTORE and SSTORE.REVERT happens together and in FILO order when is_persistant = 0, which ensures all storage writes to be undone correctly。
evm circuit中的SSTORE伪代码类似为:
// read location and value
bus_mapping_lookup(gc, Stack, sp, loc, Read)
bus_mapping_lookup(gc+1, Stack, sp+1, val, Read)
// write storage
bus_mapping_lookup(gc+2, Storage, loc, val, Write, val_prev, is_first_touch)
if !is_persistant {
// rollback storage in a "first in last out" order
bus_mapping_lookup(
gc_end_of_call - sstore_counter,
Storage,
loc,
val_prev,
Write,
val,
is_first_touch,
)
}
// sstore_counter should increase 1
sstore_counter_next === sstore_counter + 1
gc_next === gc + 3 // gc should increase 3
pc_next === pc + 1 // pc should increase 1
sp_next === sp + 2 // sp should increase 2 (SSTORE = 2 POP)
evm circuit中的RETURN和REVERT伪代码类似为:
// read offset and size (of return data)
bus_mapping_lookup(gc, Stack, sp, offset, Read)
bus_mapping_lookup(gc+1, Stack, sp+1, size, Read)
// TODO: check offset and size are set in parent's context for RETURNDATA
// should be persistant if RETURN
if op == RETURN {
is_persistant === 1
}
// should not be persistant if REVERT
if op == REVERT {
is_persistant === 0
}
// check no extra records within revert section
gc_end_of_call === gc + 1 + sstore_counter
// gc should jump to correct one
gc_next === gc_end_of_call + 1
// TODO: pc_next should set back to parent's next one
// TODO: sp_next should set back to parent's next one
4.2 Smaller but Complete Example
若有如下trace:
PC OP STACK STORAGE
---- --------- ---------- ----------------
0 PUSH1 1 [ , 1 ] { 6: 0, a: 0 }
2 PUSH1 a [ a, 1 ] { 6: 0, a: 0 }
4 SSTORE [ , ] { 6: 0, a: 1 }
5 PUSH1 3 [ , 3 ] { 6: 0, a: 1 }
7 PUSH1 6 [ 6, 3 ] { 6: 0, a: 1 }
9 SSTORE [ , ] { 6: 3, a: 1 }
10 PUSH1 0 [ , 0 ] { 6: 3, a: 1 }
12 PUSH1 0 [ 0, 0 ] { 6: 3, a: 1 }
14 REVERT [ 0, 0 ] { 6: 0, a: 0 }
则根据gc增序排列的bus mapping table为:【为简单起见,忽略了is_first_touch,其仅在 gc=5 和 gc=10 的行对应值才为1。】
相关context为:
- gc_end_of_call=16
- is_persistant=0
上图中,绿色的行对应为相同的 4 SSTORE
,具体为:
bus_mapping_lookup(5, Storage, a, 1, Write, 0, 1)
// gc = gc_end_of_call - sstore_counter = 16 - 0 = 16
bus_mapping_lookup(16, Storage, a, 0, Write, 1, 0)
sstore_counter_next === sstore_counter + 1 // sstore_counter_next = 0 + 1 = 1
橙色的行对应为相同的 9 SSTORE
,具体为:
bus_mapping_lookup(10, Storage, 6, 3, Write, 0, 1)
// gc = gc_end_of_call - sstore_counter = 16 - 1 = 15
bus_mapping_lookup(15, Storage, 6, 0, Write, 3, 0)
sstore_counter_next === sstore_counter + 1 // sstore_counter_next = 1 + 1 = 2
当REVERT时,至少需做如下check:
// no extra records within revert section
gc_end_of_call === gc + 1 + sstore_counter // 16 = 13 + 1 + 2
// next gc jump to after gc_end_of_call
gc_next === gc_end_of_call + 1
4.3 Update Storage Root on L1 Contract
For all identical locations of address:【注意其中的build 和 open 动作为设计circuit中的merkle proof verification。】
- the init rows will be attached with a merkle proof to ensure it’s read from old state root.
- the first one in total will be verified with the old state root as a public input from L1 contract.
- the last row of each location builds a intermediate state root using the same merkle proof of its init row with its final value.
- next location opens the intermediate state root to read its old value. 原因在于we know different locations will no be updated due to the location group constraint.
- in the end of storage circuit, 可build the finalized state root. 然后use a public input to verify the equality of the result, and update it to L1 contract。
4.4 QA
Q1: How to handle other world state update?
会引起state tire update的操作有:
- 1)Transaction—— nonce 和 balance 将更新
- 2)CALL——若CALL具有non-zero value,则balance 将更新
- 3)CREATE, CREATE2——hash of code将更新到新的合约地址
- 4)SSTORE——上文已提及。
Q2: How to handle errors caused not by REVERT?
// List evm execution errors
var (
ErrOutOfGas = errors.New("out of gas")
ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas")
ErrDepth = errors.New("max call depth exceeded")
ErrInsufficientBalance = errors.New("insufficient balance for transfer")
ErrContractAddressCollision = errors.New("contract address collision")
ErrExecutionReverted = errors.New("execution reverted")
ErrMaxCodeSizeExceeded = errors.New("max code size exceeded")
ErrInvalidJump = errors.New("invalid jump destination")
ErrWriteProtection = errors.New("write protection")
ErrReturnDataOutOfBounds = errors.New("return data out of bounds")
ErrGasUintOverflow = errors.New("gas uint64 overflow")
ErrInvalidCode = errors.New("invalid code: must not begin with 0xef")
)
若要完全支持evm,则在circuit设计中应支持所有可能的error类型。如使用a 10-bit lookup to check sp’s validity will be super simple,但是,这将使Prover无法为stack overflow或stack underflow等错误构建相应的proof。
这些error都将halt the call and lead to all storage updates rollback just like REVERT, 所不同的是它们将消耗所有的given gas。
因此,可将所有的错误类型做为execution result of a op来处理,让Prover来show us which result it is并 证明之。如,POP可有1种success result 和2种error result——ErrStackUnderflow和ErrOutOfGas。
Q3: How to handle dynamic gas due to access list (EIP2929)?
可在bus mapping 中额外引入一个元素 is_first_touch,用于specify which time it is being access,然后在evm circuit中,根据access time来调整gas cost。
由于 EIP2929为per EOA transaction,需在storage circuit中知悉,因此需要一个root_call_context来enable the is_first_touch flag。
5. Bus Mapping
memory circuit和stack circuit将为bus mapping lookup table 提供 valid and meaningful access record,该bus mapping lookup table将在state circuit和evm circuit中共享。
具有:
- 唯一的
gc
来作为synchronizing clock target
来specify the residue of the access record- 若有需要,有任意数量的
valX
在evm circuit中,通过lookup all gc
one by one and finally check the bus mapping degree is bounded to gc
in the execution end,然后就可相信没有插入恶意的write。
target
和相应的valX
为:
-
Stack:
– val1 - key
– val2 - val
– val3 - rw -
Memory:
– val1 - key
– val2 - val
– val3 - rw -
Storage:
– val1 - key
– val2 - val
– val3 - rw
– val4 - val_prev
– val5 - is_first_touch
对应以上solidity例子的bus mapping(按gc增序排列)为:
6. Call Context
在EOA call和internal call中,其stack和memory都是通过call_context来分隔,主要有2方面的好处:
- caller 和 callee 不需要复制the return data or call data if not used。但使用 CALLDATACOPY 或 RETURNDATACOPY 时,仅需locate the memory by caller or callee’s call_context,and ensure they are indeed there。
- callee可memorize caller’s call_context directly in evm circuit,然后解压回caller’s context。
具体见 appliedzkp的zkevm(4)Call context。
7. Q&A
参考资料
[1] https://hackmd.io/Nwd0e5AgTVSBWRQlp-Ving
[2] 以太坊黄皮书
[3] understanding mload assembly function
[4] https://hackmd.io/kON1GVL6QOC6t5tf_OTuKA
以上是关于appliedzkp的zkevmState Circuit的主要内容,如果未能解决你的问题,请参考以下文章
appliedzkp的zkevm定制化Proof System