Polygon zkEVM ROM 是一个zkASM 语言实现的EVM,实现了对BATCH
进行执行处理的过程,主要分为以下几步:
- A:将初始的寄存器的值载入内存中:
oldStateRoot(B)
,oldAccInputHash(C)
,oldNumBatch(SP),chainID(GAS)
. - B: 设置
Batch
全局变量; - C: 循环解析RLP交易;
- D: 循环处理交易;
- E:
Batch
计算:获取newLocalExitRoot
, 断言transactions size
, 计算batchHashData
和newAccInputHash
.
载入输入变量
在Main_executor 中初始化状态时,首先对寄存器B, C, SP, GAS
进行初始化,如下所示:
// Set oldStateRoot to register B
[
pols.B0[0],
pols.B1[0],
pols.B2[0],
pols.B3[0],
pols.B4[0],
pols.B5[0],
pols.B6[0],
pols.B7[0]
] = scalar2fea(ctx.Fr, Scalar.e(ctx.input.oldStateRoot));
// Set oldAccInputHash to register C
[
pols.C0[0],
pols.C1[0],
pols.C2[0],
pols.C3[0],
pols.C4[0],
pols.C5[0],
pols.C6[0],
pols.C7[0]
] = scalar2fea(ctx.Fr, Scalar.e(ctx.input.oldAccInputHash));
// Set oldNumBatch to SP register
pols.SP[0] = ctx.Fr.e(ctx.input.oldNumBatch)
// Set chainID to GAS register
pols.GAS[0] = ctx.Fr.e(ctx.input.chainID)
然后通过zkASM指令读取寄存器的值到内存中:
;;;;;;;;;;;;;;;;;;
STEP => A // 断言初始STEP 为 0
0 :ASSERT ; Ensure it is the beginning of the execution
B :MSTORE(oldStateRoot)
C :MSTORE(oldAccInputHash)
SP :MSTORE(oldNumBatch)
GAS :MSTORE(chainID)
以下将相关的输入保存在到内存中:
${getGlobalExitRoot()} :MSTORE(globalExitRoot) // 保存globalExitRoot
${getSequencerAddr()} :MSTORE(sequencerAddr) // 保存sequencerAddr
${getTimestamp()} :MSTORE(timestamp) // 保存timestamp
${getTxsLen()} :MSTORE(batchL2DataLength) //保存batchL2DataLength
B => SR ;set initial state root // 赋值SR寄存器为State Root
; Increase batch number
SP + 1 :MSTORE(newNumBatch) // 保存newNumBatch
设置Batch
全局变量
首先判断是否需要设置globalExitRoot
.
$${eventLog(onStartBatch, C)} // 日志打印信息
$ => A :MLOAD(globalExitRoot)
0 => B
$ :EQ, JMPC(skipSetGlobalExitRoot)
若globalExitRoot
为0, 则不需要再设置globalExitRoot
.
setGlobalExitRoot
首先计算 keccak256(globalExitRoot, 0)
0 => HASHPOS // HashK 输入数据的起始位置
$ => E :MLOAD(lastHashKIdUsed) // HashK id
E+1 => E :MSTORE(lastHashKIdUsed)
32 => D // 定义本次HashK 输入数据的长度
A :HASHK(E) // HashK 输入寄存器A中的值
// HashK 输入常量 0
%GLOBAL_EXIT_ROOT_STORAGE_POS :HASHK(E) ; Storage position of the global exit root map
HASHPOS :HASHKLEN(E) // 根据HASHPOS的最终长度计算 HashK
$ => C :HASHKDIGEST(E) // 将HashK 计算的hash值保存在寄存器 C 上
然后以寄存器A,B,C
的值计算key, 从SMT 树读取相应的值,保存在寄存器D
中。若读取的值不为0,则进行跳转。
%ADDRESS_GLOBAL_EXIT_ROOT_MANAGER_L2 => A //GLOBAL_EXIT_ROOT_MANAGER_L2 合约地址
%SMT_KEY_SC_STORAGE => B // 常量值3
; read timestamp given the globalExitRoot
; skip overwrite timestamp if it is different than 0
$ => D :SLOAD, JMPNZ(skipSetGlobalExitRoot)
在SSTORE
指令中,以A,B,C
寄存器计算key, D
寄存器中的值为value
, 保存在SMT 存储中,最后将SMT更新后得到的新的状态根保存在SR
寄存器中。
$ => D :MLOAD(timestamp)
$ => SR :SSTORE ; Store 'timestamp' in storage position 'keccak256(globalExitRoot, 0)'
skipSetGlobalExitRoot
从SMT中读取交易的数量,并保存在内存中。
SR :MSTORE(batchSR) // 读取当前的state root 到内存中
; Load current tx count
%LAST_TX_STORAGE_POS => C // 常量为0
%ADDRESS_SYSTEM => A // 系统合约:0x000000000000000000000000000000005ca1ab1en
%SMT_KEY_SC_STORAGE => B // 常量3
$ => D :SLOAD
D :MSTORE(txCount)
首先根据交易数据长度,除以136(每轮KecccakF 处理1088位),得到当前Batch需要的KeccakF个数,和最允许的最大个数进行比较。若超过范围,则报错。
; Compute necessary keccak counters to finish batch
$ => A :MLOAD(batchL2DataLength)
; Divide the total data length + 1 by 136 to obtain the keccak counter increment.
; 136 is the value used by the prover to increment keccak counters
A + 1 :MSTORE(arithA)
136 :MSTORE(arithB), CALL(divARITH); in: [arithA, arithB] out: [arithRes1: arithA/arithB, arithRes2: arithA%arithB]
$ => B :MLOAD(arithRes1)
; Compute minimum necessary keccaks to finish the batch
B + 1 + %MIN_CNT_KECCAK_BATCH :MSTORE(cntKeccakPreProcess) //MIN_CNT_KECCAK_BATCH 为1
%MAX_CNT_KECCAK_F - CNT_KECCAK_F => A // MAX_CNT_KECCAK_F 为2376
$ :LT, JMPC(outOfCountersKeccak)
循环解析RLP交易
载入RLP 编码的交易数据,保存RLP 编码正确性,若有任意一个交易的RLP编码出现问题,则不会处理任何交易 。
;;;;;;;;;;;;;;;;;;
E+1 => E :MSTORE(lastHashKIdUsed) // 更新last HashK id used
0 :MSTORE(batchHashPos) // bashHash 的起始位置
E :MSTORE(batchHashDataId) // bashHashData的id
$ => A :MLOAD(lastCtxUsed) // 读取 lastCtxUsed
A :MSTORE(ctxTxToUse) ; Points at first context to be used when processing transactions // 保存使用的 ctx
$${var p = 0} // 在js中定义变量p的值为0, p为指向batchL2Data的offset
txLoopRLP
batchL2Data
的交易数据格式为:
rlp(nonce, gasprice, gaslimit, to, value, data, chainId, 0, 0)|r|s|v| rlp(...)|r|s|v|...
对于解析的每笔交易,CTX
增1 。然后从内存中读取总的交易数据长度batchL2DataLength
和 已经解析的交易数据长度batchL2DataParsed
. 若没解析完,则跳转到loadTx_rlp
; 若解析完成,则跳转到endChekcRLP
处。
txLoopRLP:
$ => A :MLOAD(lastCtxUsed)
A+1 => CTX :MSTORE(lastCtxUsed) // 每笔交易占用一个CTX 空间
$ => A :MLOAD(batchL2DataLength)
$ => C :MLOAD(batchL2DataParsed)
C - A :JMPN(loadTx_rlp, endCheckRLP)
endCheckRLP:
:JMP(txLoop)
loadTx_rlp
RLP 解析包含五步,分别为:
- A : 初始化;
- B: 读取并且检查RLP各个字段,填充
batchHashData
和 以太坊签名的交易字节数据; - C: 读取签名,填充
batchHashData
; - D: 完成RLP 解析;
- E: RLP的错误处理
初始化
对于每笔交易,解析的数据为:
[rlp(nonce, gasprice, gaslimit, to, value, data, chainId, 0, 0)|r|s|v]
签名的以太坊交易为:
H_keccak(rlp(nonce, gasprice, gaslimit, to, value, data, chainId, 0, 0))
初始化过程为如下:
; A new hash with position 0 is started
0 => HASHPOS // 将HASHPOS 置为0
; We get a new hashId
$ => E :MLOAD(lastHashKIdUsed)
E+1 => E :MSTORE(lastHashKIdUsed) // E寄存器存储新的HashK id
; Pointer to next RLP bytes to read
0 => C
解析RLP字段
首先理解两个调用的函数addHashTx
和 addBatchHashData
对于addHashTx
用于从batchL2Data
读取D
长度的数据,输入到HashK中。
;; Add bytes to generate ethereum transaction hash. transactionHash = H_keccak(rlp(nonce, gasprice, gaslimit, to, value, data, chainId, 0, 0))
addHashTx:
$ => A :MLOAD(batchL2DataLength) // 获取batchL2Data 数据总长度
$ => B :MLOAD(batchL2DataParsed) // 获取已经解析的数据长度
A - B - C - D :JMPN(invalidTxRLP) // C: 当前交易已经读取的offset, D: 本次读取的长度
${getTxs(p,D)} => A // p: batchL2Data读取位置的offset D: 本次读取的长度
$${p = p + D} // 更新p的值
A :HASHK(E) // 将从batchL2Data新读取的数据输入到HashK中,用于计算交易Hash
C + D => C :RETURN // 更新C 偏移量的值
对于addBatchHashData
, 将从batchL2Data
读取的数据,用于计算batchHashData
.
;@info: Add 'data' bytes to batchHashData. batchHashData = H_keccak( transactions )
;@in: D: length of the hash
addBatchHashData:
$ => HASHPOS :MLOAD(batchHashPos) // 读取batchHashPos
$ => E :MLOAD(batchHashDataId) // 读取batchHashData id
A :HASHK(E) // 读取的数据输入到HashK
HASHPOS :MSTORE(batchHashPos) // 更新batchHashPos
C => HASHPOS // 将HASHPOS 置为当前交易读取位置的offset
$ => E :MLOAD(lastHashKIdUsed), RETURN // 将E重置为当前交易的HashK id
利用上面两个函数,首先读取batchL2Data
的第一个字节,若其值小于0xc0
, 则说明不是数组,RLP编码无效。
;; Read RLP list length
; Should be a list
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0xc0 :JMPN(invalidTxRLP)
A - 0xf8 :JMPN(shortList) // 根据RLP编译,若值小于f8, 则于短数组(长度不大于55)
对于长列表,需从batchL2Data
读取 RLP 列表的实际长度,对于短列表,直接计算出交易RLP列表实际长度。
longList:
A - 0xf7 => D :CALL(addHashTx)
:CALL(addBatchHashData)
:JMP(endList)
shortList:
A - 0xc0 => A // 对于短列表,获取RLP编译的实现长度
endList:
A + C => B :MSTORE(txRLPLength) // 保存RLP编码后交易的总长度
; Check enough zk counters to digest tx hash
B + 1 :MSTORE(arithA)
136 :MSTORE(arithB), CALL(divARITH); in: [arithA, arithB] out: [arithRes1: arithA/arithB, arithRes2: arithA%arithB]
$ => B :MLOAD(arithRes1)
$ => D :MLOAD(cntKeccakPreProcess)
%MAX_CNT_KECCAK_F - CNT_KECCAK_F - B - D - 1:JMPN(handleOOCKatRLP)
解析 nonce
从batchL2Data
读取一个字节,若其值小于0x80, 则其值为txNonce
; 若为0x80,则txNonce
值为0, lengthNonce
值为0; 若于小于0x89, 则再次从batchL2Data
读取txNonce
.
txNonce
和 lengthNonce
保存在CTX
中, CTX
在内存的起始位置为: 0x040000
;; Read RLP 'nonce'
; 64 bits max //最多8个字节
nonceREAD:
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0x80 :JMPN(endNonce)
A - 0x81 :JMPN(nonce0)
A - 0x89 :JMPN(shortNonce, invalidTxRLP)
nonce0:
0 => A :MSTORE(lengthNonce), JMP(endNonce)
shortNonce:
A - 0x80 => D //lenthNonce表示txNonce的字节长度
D :MSTORE(lengthNonce), CALL(addHashTx)
:CALL(addBatchHashData)
endNonce:
A :MSTORE(txNonce)
解析 gas price
从batchL2Data
读取gas price
, 并保存CTX
中的 txGasPriceRLP
位置。
;; Read RLP 'gas price'
; 256 bits max // 最多32个字节
gasPriceREAD:
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0x80 :JMPN(endGasPrice)
A - 0x81 :JMPN(gasPrice0)
A - 0xa1 :JMPN(shortGasPrice, invalidTxRLP)
gasPrice0:
0 => A :JMP(endGasPrice)
shortGasPrice:
A - 0x80 => D
D - 1 :JMPN(endGasPrice)
:CALL(addHashTx)
:CALL(addBatchHashData)
endGasPrice:
A :MSTORE(txGasPriceRLP)
解析 gas limit
从batchL2Data
读取gas limit
, 并保存CTX
中的 txGasLimit
位置。
;; Read RLP 'gas limit'
; 256 bits max // 最大32 个字节
gasLimitREAD:
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0x80 :JMPN(endGasLimit)
A - 0x81 :JMPN(gasLimit0)
A - 0xa1 :JMPN(shortGasLimit, invalidTxRLP)
gasLimit0:
0 => A :JMPN(endGasLimit)
shortGasLimit:
A - 0x80 => D
D - 1 :JMPN(endGasLimit)
:CALL(addHashTx)
:CALL(addBatchHashData)
endGasLimit:
A :MSTORE(txGasLimit)
解析 to
若to
长度为0,则表示为创建合约的交易,将isCreateContract
设为1; 若长度以20, 则将 to
地址保存到 txDestAddr
和storageAddr
中。
isCreateContract, txDestAddr, storageAddr
皆保存在CTX中。
;; Read RLP 'to'
; 160 bits max // 20字节或0
toREAD:
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0x80 :JMPN(invalidTxRLP)
A - 0x81 :JMPN(noTo)
A - 0x94 :JMPN(invalidTxRLP)
A - 0x95 :JMPN(shortTo, invalidTxRLP)
noTo:
1 :MSTORE(isCreateContract), JMP(endTo)
shortTo:
A - 0x80 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A :MSTORE(txDestAddr)
A :MSTORE(storageAddr)
endTo:
解析 value
从batchL2Data
读取value
, 并保存CTX
中的 txValue
位置。
;; Read RLP 'value'
; 256 bits max // 最大32个字节
valueREAD:
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0x80 :JMPN(endValue)
A - 0x81 :JMPN(value0)
A - 0xa1 :JMPN(shortValue, invalidTxRLP)
value0:
0 => A
:JMPN(endValue)
shortValue:
A - 0x80 => D
D - 1 :JMPN(endValue)
:CALL(addHashTx)
:CALL(addBatchHashData)
endValue:
A :MSTORE(txValue)
解析 data
解析RLP中的交易data 数据,通过SP
寄存器保存在 EVM Stack, 在内存的起始的位置为:0x040000 + 0x010000 + CALLDATA_OFFSET(1024)
。
将解析后数据长度保存CTX中的txCalldataLen
位置,数据保存在EVM Stack中,每个位置保存32字节,左边为低位,右边为高位。
注:dataStarts 用于表示data真正数据在batchL2Data
的位置,保存为CTX中的数据,在内存的起始位置为:0x40000 + 38
, 其中0x40000
为CTX起始位置,38为 dtataStarts
的 offset.
;; Read RLP 'data'
; should not be a list
dataREAD:
$ => D :MLOAD(batchHashPos)
D :MSTORE(dataStarts) // 设置dataStarts 为 batchHashPos
1 => D //首先读到data rlp 编码的第1个字节
%CALLDATA_OFFSET => SP :CALL(addHashTx) //CALLDATA_OFFSET 值为1024
:CALL(addBatchHashData)
A - 0x80 :JMPN(veryShortData) // data为单字节
A - 0x81 :JMPN(endData) // data为空
A - 0xb8 :JMPN(shortData) // data 长度不大于55个字节
A - 0xc0 :JMPN(longData, invalidTxRLP) // data 长度大于55个字节
veryShortData:
1 :MSTORE(txCalldataLen) // data 长度为1
31 => D :CALL(SHLarith) // SHLarith 功能: A << D, 将A 往左移 D字节
A :MSTORE(SP++), JMP(endData) // 将A存在EVM Stack中
shortData:
$ => D :MLOAD(batchHashPos)
D :MSTORE(dataStarts) // 更新 dataStarts 为 batchHashPos
A - 0x80 => B :MSTORE(txCalldataLen), JMP(readData)
longData:
A - 0xb7 => D :CALL(addHashTx)
:CALL(addBatchHashData)
$ => D :MLOAD(batchHashPos)
D :MSTORE(dataStarts) // 更新 dataStarts 为 batchHashPos
A => B :MSTORE(txCalldataLen) // 解析后 data 的数据长度
readData:
32 => D
B - D :JMPN(readDataFinal) // 若最后一部数据小于32字节,则跳转
B - D :MSTORE(txDataRead), CALL(addHashTx) //txDataRead 为要读取data的剩余长度
A :MSTORE(SP++), CALL(addBatchHashByteByByte)
//将读取的32字节数据保存在EVM Stack,位置根据SP确定,同时将读到的数据添加到BatchHashData中。
$ => B :MLOAD(txDataRead), JMP(readData) // 循环读取32字节
readDataFinal:
B - 1 :JMPN(endData) // 确保最后一部data的长度大于等于1
B => D :CALL(addHashTx)
32 - D => D :CALL(SHLarith) // 将读到的A寄存器器值左移 D 字节
A :MSTORE(SP) // 保存在EVM Stack 中
32 - D => D :CALL(addBatchHashByteByByte) //将最后一部分data 添加到BatchHashData中
endData:
; Check all bytes read to detect pre EIP-155 tx
C => A
$ => B :MLOAD(txRLPLength)
$ :EQ,JMPC(setPreEIP155Flag) // 判断是否为EIP155前的交易类型,即不含(ChainId,0,0)
解析 chainId
从batchL2Data
读取 chainId
, 保存CTX中的txChainID
位置。
;; Read RLP 'chainId'
; 64 bits max //最大8个字节
chainREAD:
1 => D :CALL(addHashTx)
:CALL(addBatchHashData)
A - 0x80 :JMPN(endChainId)
A - 0x81 :JMPN(chainId0)
A - 0x89 :JMPN(shortChainId, invalidTxRLP)
chainId0:
0 => A :JMP(endChainId)
shortChainId:
A - 0x80 => D
D - 1 :JMPN(endChainId)
:CALL(addHashTx)
:CALL(addBatchHashData)
endChainId:
A :MSTORE(txChainId)
解析(0,0)
解析最后两个字节的数据为:0x8080
.
;; Read RLP last two values (0, 0)
; 64 bits max
2 => D :CALL(addHashTx)
:CALL(addBatchHashData)
0x8080 => B
$ :EQ,JMPC(sizeVerification, invalidTxRLP)
size verification
setPreEIP155Flag:
1 :MSTORE(isPreEIP155) // 用于表示为EIP155 之前的交易类型
;; size verification
; checks RLP lenght read at the RLP header with bytes read during RLP parsing
sizeVerification:
C => A
$ => B :MLOAD(txRLPLength) // 比较RLP编码后的长度
$ :EQ,JMPC(sizeVerificationSuccess, invalidTxRLP)
sizeVerificationSuccess:
HASHPOS :HASHKLEN(E) // 计算交易的Hash值
签名解析
分别读取txR, txS, txV
, 保存到CTX中,并添加到BatchHashData
中。
;; read ecdsa 'r'
rREADTx:
32 => D :CALL(getTxBytes)
A :MSTORE(txR)
C + D => C :CALL(addBatchHashData)
;; read ecdsa 's'
sREADTx:
32 => D :CALL(getTxBytes)
A :MSTORE(txS)
C + D => C :CALL(addBatchHashData)
;; read ecdsa 'v'
vREADTx:
1 => D :CALL(getTxBytes)
A :MSTORE(txV)
C + D => C :CALL(addBatchHashData)
完成RLP 交易解析
首先更新已经解析的交易数据长度batchL2DataParsed
, 将pendingTxs
个数增1 ,将交易hash 保存在CTX中。
;;;;;;;;;
;; D - Finish RLP parsing
;;;;;;;;;
;; update bytes parsed
$ => A :MLOAD(batchL2DataParsed)
A + C => A :MSTORE(batchL2DataParsed)
;; increase number of transaction to process
$ => A :MLOAD(pendingTxs)
A + 1 => A :MSTORE(pendingTxs)
;; compute signature
$ => A :HASHKDIGEST(E)
A :MSTORE(txHash), JMP(txLoopRLP)
最后跳转,去解析下一个RLP交易.
循环处理交易
txLoop
分别载入每个交易的数据,并对其进行解释。
;;;;;;;;;;;;;;;;;;
;; D - Loop processing transactions
;; - Load transaction data and interpret it
;;;;;;;;;;;;;;;;;;
txLoop:
$ => A :MLOAD(pendingTxs)
A-1 => A :MSTORE(pendingTxs), JMPN(processTxsEnd)
$ => A :MLOAD(ctxTxToUse) ; Load first context used by transaction //跳转到交易 CTX
A+1 => CTX :MSTORE(ctxTxToUse),JMP(processTx)
processTxEnd:
:CALL(updateSystemData)
processTxFinished:
$${eventLog(onFinishTx)} :JMP(txLoop)
processTxsEnd:
processTx
交易的处理主要分为以下几步:
- A: 验证
ecdsa
签名; - B: 验证
tx.sender
; - C: 验证
chainID
; - D: 验证和增加
nonce
; - E: 验证预付花费;
- F: 检查交易类型
- 调用合约
- 部署合约
- 处理
gas
.
验证 ecdsa 签名
根据txR, txS, txV, txHash
, 通过ecrecover
计算 from
地址,将txSrcAddr
保存在交易CTX中,txSrcOriginAddr
保存在非交易CTX的内存中。
;;;;;;;;;;;;;;;;;;
$${eventLog(onProcessTx)} // 日志打印
;;;;;;;;;
;; Store init state
;;;;;;;;;
; Store initial state at the beginning of the transaction
SR :MSTORE(originSR) //交易处理前的状态根 全局
SR :MSTORE(initSR) //CTX: 处理完nonce和预付cost后的根
; Minimum of 100000 steps left to process a tx
%MAX_CNT_STEPS - STEP - 100000 :JMPN(outOfCountersStep) //MAX_CNT_STEPS: 2^23 - 200 = 8388408
%MAX_CNT_BINARY - CNT_BINARY - 100 :JMPN(outOfCountersBinary) //MAX_CNT_BINARY: 2^23 /16
$ => A :MLOAD(txHash) // 读取txHash
; Check the signature
$ => B :MLOAD(txR) // 读取 txR
$ => C :MLOAD(txS) // 读取 txS
$ => D :MLOAD(txV), CALL(ecrecover_tx) // 调用ecrecover 函数
; Check result is non-zero
checkAndSaveFrom:
0 => B
A :MSTORE(txSrcAddr)
A :MSTORE(txSrcOriginAddr)
$ :EQ,JMPC(invalidIntrinsicTxSignature)
若计算的from
地址为0,则为无效的交易。
验证 tx.sender
根据A,B,C
计算key, 从SMT 中获取 from
地址合约代码是否为空,以确保 from
不是合约地址。
;;;;;;;;;;;;;;;;;;
;; B - Verify tx.sender does not have any code deployed (EIP3607)
;;;;;;;;;;;;;;;;;;
%SMT_KEY_SC_LENGTH => B //SMT_KEY_SC_LENGTH: 4
0 => C
$ => B :SLOAD, JMPNZ(invalidIntrinsicTxSenderCode)
验证 chainID
对于EIP155之前的交易类型,不再验证chainId.
对于EIP155之后的交易类型,主要验证交易的chainId 和 Batch chainId是否一致。
;;;;;;;;;;;;;;;;;;
; Don't check chainId for legacy transactions
$ => A :MLOAD(isPreEIP155), JMPNZ(endCheckChainId)
$ => A :MLOAD(txChainId) // 交易的chainId
$ => B :MLOAD(chainID) // batch chainID
$ :EQ,JMPC(endCheckChainId, invalidIntrinsicTxChainId)
; If A == B --> endCheckChainId
endCheckChainId:
;; Reset warm/cold information
$ => A :MLOAD(txSrcOriginAddr), CALL(initTouchedTree)
:CALL(isColdAddress) ; add tx.origin to touched addresses
0 :MSTORE(depth) ; Initial depth is 0
;; Set gasPrice global var
$ => A :MLOAD(txGasPriceRLP) // txGasPriceRLP 在CTX 中
A :MSTORE(txGasPrice) // txGasPrice 为全局变量
将 tx.origin
添加到 touched address 地址中,TouchedTree 采用SMT树存储,每个交易单独构建, 根为touchedSR
, 每个地址的key由tx.origin
, SMT_KEY_TOUCHED_ADDR
(常量5), 0
(内部存储key) 三部分计算, value 为 0 表示为cold address,为1 表 warm address。
checkWarmed:
; save current state root & load touched root
SR :MSTORE(tmpStateRoot)
$ => SR :MLOAD(touchedSR)
; read from touched tree if address is warm
; A register already set
%SMT_KEY_TOUCHED_ADDR => B
0 => C
$ => D :SLOAD, JMPZ(markWarmAddress)
注:零地址和预编译合约(地址小于 10)为warm address.
验证 nonce
比较SMT 中 from
地址中的 nonce
值 和 交易中的nonce
是否一致;
然后将nonce
增加 1 保存到 SMT中,返回新的状态机到SR
中。
$ => A, E :MLOAD(txSrcOriginAddr) ; Address of the origin to A and E
%SMT_KEY_NONCE => B // SMT_KEY_NONCE 值为1
0 => C
$ => A :SLOAD
$ => B :MLOAD(txNonce)
$ => C :EQ, JMPNC(invalidIntrinsicTxNonce) ; Compare nonce state tree with nonce transaction
B :ASSERT ; sanity check
A + 1 => D
E => A
%SMT_KEY_NONCE => B
0 => C
$ => SR :SSTORE ; Store the nonce plus one
验证预付 cost
对于部署合约: gas Limit >= 53000 + calldata cost
对于调用合约: gasLimit >= 2100 + calldata cost;
对于calldata: 分别累加每个字节的 gas, 若字节为0, 则加4, 否则加16
然后验证: balance >= gas price * gas limit + value
再更新from
余额为:balance -(gas price * gas limit )
最后将未用完的 gas 保存在 GAS
寄存器中。
; Verify batch gas limit
$ => B :MLOAD(txGasLimit)
; Check batch gas limit is not exceeded by transaction
%TX_GAS_LIMIT => A // 交易最大的Gas limit: 30000000
$ :LT,JMPC(invalidIntrinsicBatchGasLimit)
; Intrinsic gas --> gas Limit >= 21000 + calldata cost + deployment cost
%BASE_TX_GAS => E ; Store init intrinsic gas at E
$ => A :MLOAD(isCreateContract), JMPNZ(addDeploymentGasCost, getCalldataGasCost)
addDeploymentGasCost:
%BASE_TX_DEPLOY_GAS => E ; 53000 gas if transaction is a create
getCalldataGasCost:
$ => A :MLOAD(txCalldataLen)
0 => B
$ :EQ,JMPC(endCalldataIntrinsicGas)
addGas:
$ => HASHPOS :MLOAD(dataStarts)
0 => C :JMP(loopBytes)
loopBytes:
%MAX_CNT_STEPS - STEP - 10 :JMPN(outOfCountersStep)
A - C - 1 :JMPN(endCalldataIntrinsicGas)
E => B
$ => E :MLOAD(batchHashDataId)
$ => D :HASHK1(E)
B => E
C + 1 => C
D - 1 :JMPN(add4Gas, add16Gas)
add4Gas:
E + 4 => E :JMP(loopBytes)
add16Gas:
E + 16 => E :JMP(loopBytes)
endCalldataIntrinsicGas:
; Compare gas limit >= intrinsic gas
$ => A :MLOAD(txGasLimit)
E => B
$ :LT, JMPC(invalidIntrinsicTxGasLimit)
; Store calculated gas for later usage
E :MSTORE(gasCalldata)
; Check upfront cost: balance >= gas price * gas limit + value
; gas price * gas limit
$ => B :MLOAD(txGasPrice)
A :MSTORE(arithA)
B :MSTORE(arithB), CALL(mulARITH)
$ => D :MLOAD(arithOverflow), JMPNZ(invalidIntrinsicTxGasOverflow)
$ => D :MLOAD(arithRes1)
; Get caller balance
$ => A :MLOAD(txSrcOriginAddr)
0 => B, C
$ => C :SLOAD
; (gas price * gas limit) + value
$ => B :MLOAD(txValue)
D :MSTORE(arithA)
B :MSTORE(arithB), CALL(addARITH)
$ => B :MLOAD(arithRes1)
; Comparison
C => A
$ :LT, JMPC(invalidIntrinsicTxBalance)
; Substract (gas price * gas limit) from caller balance
C :MSTORE(arithA)
D :MSTORE(arithB), CALL(subARITH)
; Substracted balance result in D
$ => D :MLOAD(arithRes1)
$ => A :MLOAD(txSrcOriginAddr)
0 => B,C
$ => SR :SSTORE
; Store state root with upfront cost substracted and nonce increased
SR :MSTORE(initSR)
; Substract intrinsic gas
$ => GAS :MLOAD(txGasLimit)
$ => A :MLOAD(gasCalldata)
GAS - A => GAS
检查交易类型
首先确定交易类型:
若 to
为空,则为部署合约交易;
将 to
地址加入到touched addresses
;
若to
地址小于10, 则为预编译合约;
其它的为普通的合约调用。
txType:
; Compute deployment address if create contract operation
$ => A :MLOAD(isCreateContract), JMPNZ(getContractAddress) // 部署合约交易
$ => A :MLOAD(txDestAddr)
:CALL(isColdAddress) ; Add 'to' to touched addresses
; Check 'to' is zero or precompiled contract
; Check zero address since zero address is not a precompiled contract
0 => B
$ :EQ, JMPC(callContract) // 调用地址为0的合约
10 => B
$ :LT,JMPC(selectorPrecompiled, callContract) // 预编译合约和普通的合约调用
部署合约交易
计算合约地址
对于create
, 合约地址为:keccak256(rlp.encode([normalize_address(sender), nonce]))[12:]
对于create2
, 合约地址为:keccak256( 0xff ++ senderAddress ++ salt ++ keccak256(init_code))[12:]
当于create
的情况下:
;; compute new create address
getContractAddress:
; A new hash with position 0 is started
0 => HASHPOS
; We get a new hashId
$ => E :MLOAD(lastHashKIdUsed) // 获取新的HashK id
E+1 => E :MSTORE(lastHashKIdUsed)
; Check if create is with CREATE2 opcode
$ => A :MLOAD(isCreate2), JMPNZ(create2) // 判断是否为create2
; Check keccak counters
$ => A :MLOAD(cntKeccakPreProcess)
%MAX_CNT_KECCAK_F - CNT_KECCAK_F - A - 1:JMPN(outOfCountersKeccak)
$ => A :MLOAD(txNonce)
0x80 => B
$ :LT,JMPC(nonce1byte) // 判断nonce是否为1个字节
$ => C :MLOAD(lengthNonce)
; 1 byte length address + 20 bytes address + 1 byte length nonce + C bytes nonce
0xc0 + 22 + C :HASHK1(E) // 计算 keccak256(rlp(address, nonce))
0x94 :HASHK1(E)
20 => D
$ => B :MLOAD(txSrcAddr)
B :HASHK(E)
0x80 + C :HASHK1(E)
C => D
A :HASHK(E), JMP(endContractAddress)
nonce1byte: // nonce 为1个字节
$ => A :MLOAD(txSrcAddr)
$ => B :MLOAD(txNonce)
0xc0 + 22 :HASHK1(E)
0x94 :HASHK1(E)
20 => D
A :HASHK(E)
B :JMPZ(nonceIs0)
B :HASHK1(E),JMP(endContractAddress)
nonceIs0: // nonce 为 0
0x80 :HASHK1(E),JMP(endContractAddress)
对于create2
情况下,合约地址创建:
;; compute new contract address as CREATE2 spec: keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:] (https://eips.ethereum.org/EIPS/eip-1014)
create2:
$ => C :MLOAD(txCalldataLen)
; Check keccak counters
C + 1 :MSTORE(arithA)
136 :MSTORE(arithB), CALL(divARITH); in: [arithA, arithB] out: [arithRes1: arithA/arithB, arithRes2: arithA%arithB]
$ => B :MLOAD(arithRes1)
$ => A :MLOAD(cntKeccakPreProcess)
; -2 because we will use one more keccack for generating contract address
%MAX_CNT_KECCAK_F - CNT_KECCAK_F - A - 2 => A
$ :LT, JMPC(outOfCountersKeccak)
$ => CTX :MLOAD(originCTX)
$ => B :MLOAD(argsOffsetCall)
loopCreate2: // 用于计算 keccak256(init_code)
; check zk-counters
%MAX_CNT_STEPS - STEP - 100 :JMPN(outOfCountersStep)
%MAX_CNT_BINARY - CNT_BINARY - 4 :JMPN(outOfCountersBinary)
C :JMPZ(create2end)
C - 32 :JMPN(endloopCreate2)
B => E :CALL(MLOAD32)
E => B
32 => D
$ => E :MLOAD(lastHashKIdUsed)
A :HASHK(E)
C - 32 => C :JMP(loopCreate2)
endloopCreate2:
B => E :CALL(MLOADX)
32 - C => D :CALL(SHRarith)
C => D
$ => E :MLOAD(lastHashKIdUsed)
A :HASHK(E)
create2end:
$ => CTX :MLOAD(currentCTX)
HASHPOS :HASHKLEN(E)
$ => C :HASHKDIGEST(E) // C 即为:用于计算 keccak256(init_code)
; new hash with position 0 is started
0 => HASHPOS // keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]
$ => E :MLOAD(lastHashKIdUsed)
E+1 => E :MSTORE(lastHashKIdUsed)
0xff :HASHK1(E)
20 => D
$ => A :MLOAD(txSrcAddr)
A :HASHK(E)
32 => D
$ => B :MLOAD(salt)
B :HASHK(E)
32 => D
C :HASHK(E)
endContractAddress:
HASHPOS :HASHKLEN(E)
$ => A :HASHKDIGEST(E), CALL(maskAddress) ; Mask address to 20 bytes
// 得到合约地址
A :MSTORE(createContractAddress)
A :MSTORE(txDestAddr)
A :MSTORE(storageAddr)
得到的合约地址保存在 createContractAddress, txDestAddr, storageAddr
位置,皆存在CTX中。
合约部署
合约部署时首先将合约保存在warm address
中;
然后判断合约地址nonce为0, 否则会产生 deployAddreCollision
错误;
然后将合约地址nonce 置为1, 并更新状态根 SR
;
若txValue
大于0, 则调用moveBalances
函数将from
资金给合约地址。
deploy:
:CALL(isColdAddress) ; add address to touched addresses
; check if address is deployable ( nonce == bytecode == 0)
A => E
; read nonce
0 => C
%SMT_KEY_NONCE => B
$ => B :SLOAD
0 => A
$ :LT,JMPC(deployAddressCollision)
; read bytecode
E => A
%SMT_KEY_SC_CODE => B
$ => B :SLOAD
0 => A
$ :LT,JMPC(deployAddressCollision)
; set contract nonce to 1
E => A
1 => D
%SMT_KEY_NONCE => B
$ => SR :SSTORE
; Move balances if value > 0 just before deploy
$ => B :MLOAD(txValue)
0 => A
zkPC+2 => RR
$ :LT, JMPC(moveBalances)
0 => PC
0 => SP :JMP(readCode)
对于readDeployBytecode
, 根据dataStarts
标记的交易data
在batchHash
的位置,读取data
, 然后执行相应的字节码。
;; read calldata bytes of a deploy transaction and process them
readDeployBytecode:
; check transaction is a deploy transaction
$ => B :MLOAD(isCreate), JMPNZ(readDeployBytecodeCreate)
; check enough bytes to read in calldata
$ => B :MLOAD(txCalldataLen)
B - PC - 1 :JMPN(defaultOpCode)
$ => HASHPOS :MLOAD(dataStarts)
HASHPOS + PC => HASHPOS
$ => E :MLOAD(batchHashDataId)
$ => RR :HASHK1(E)
$${eventLog(onOpcode(RR))}
PC + 1 => PC :JMP(@mapping_opcodes + RR)
对于CREATE/CREATE2
创建合约的情形如下:
;; read calldata bytes of a CREATE/CREATE2 call and process them
readDeployBytecodeCreate:
$ => E :MLOAD(txCalldataLen)
$ => CTX :MLOAD(originCTX)
; check enough bytes to read in memory
E - PC - 1 :JMPN(readDeployBytecodeCreateDefault)
$ => E :MLOAD(argsOffsetCall)
E + PC => E
1 => C :CALL(MLOADX)
$ => CTX :MLOAD(currentCTX)
31 => D :CALL(SHRarith)
A => RR
$${eventLog(onOpcode(RR))}
PC + 1 => PC :JMP(@mapping_opcodes + RR)
最后将bytecode
长度保存在SMT中,并更新SR
; 通过 hashPoseidonLinearFromMemory
调用,计算 hash bytecode
, 并将bytecode
通过saveContractBytecode
保存在 ctx.input.contractBytecode
中。
;; Compute and save hash bytecode and bytecode length in the state-tree
endDeploy:
; called from `opRETURNDeploy` which has: C --> length, E --> offset
; only when first context ends on deploy
; If length is 0 do not modify state-tree
C :JMPZ(handleGas)
; save offset memory and length to compute hash bytecode. Read bytecode first byte
E :MSTORE(memOffsetLinearPoseidon)
C :MSTORE(memSizeLinearPoseidon), CALL(checkBytecodeStartsEF)
; check bytecode first byte != 0xEF
$ :MLOAD(startsWithEF), JMPNZ(invalidCodeStartsEF)
; set bytecode length
$ => A :MLOAD(createContractAddress)
%SMT_KEY_SC_LENGTH => B
C => D
0 => C
$ => SR :SSTORE
A :MSTORE(txDestAddr), CALL(hashPoseidonLinearFromMemory)
$ => A :MLOAD(createContractAddress)
0 => C
%SMT_KEY_SC_CODE => B
$ => SR :SSTORE
调用合约交易
检查ctx.input.contractBytecode
的hash值是否和SMT的一致。
callContract:
; Move balances if value > 0 just before executing the contract CALL
$ => B :MLOAD(txValue)
0 => A
zkPC+2 => RR
$ :LT, JMPC(moveBalances)
0 => PC
0 => SP
$ => A :MLOAD(txDestAddr)
; get contract length
%SMT_KEY_SC_LENGTH => B
0 => C
$ => B :SLOAD
B :MSTORE(bytecodeLength)
0 => A
$ :EQ, JMPC(defaultOpCode) ;no bytecode
; check poseidon counters
; 56 is the value used by the prover to increment poseidon counters depending on the hash length
B :MSTORE(arithA)
56 :MSTORE(arithB), CALL(divARITH); in: [arithA, arithB] out: [arithRes1: arithA/arithB, arithRes2: arithA%arithB]
$ => B :MLOAD(arithRes1)
%MAX_CNT_POSEIDON_G - CNT_POSEIDON_G - 1 => A
$ :LT, JMPC(outOfCountersPoseidon)
$ => A :MLOAD(txDestAddr)
; get hash contract
%SMT_KEY_SC_CODE => B
$ => A :SLOAD
A :MSTORE(hashContractTxDestAddr)
0 => HASHPOS
$ => B :MLOAD(bytecodeLength)
; get a new hashPId
$ => E :MLOAD(nextHashPId)
E :MSTORE(contractHashId)
E+1 :MSTORE(nextHashPId)
; check steps used by REPEAT
%MAX_CNT_STEPS - STEP - B - 4 :JMPN(outOfCountersStep)
; set repeat itarations: RCX register + 1
B - 1 => RCX :JMPN(checkHashBytecodeEnd) // 计算 hash contract bytecode
${getBytecode(A, HASHPOS, 1)} :HASHP1(E), REPEAT(RCX) ; hash contract bytecode
checkHashBytecodeEnd:
HASHPOS :HASHPLEN(E)
$ => E :HASHPDIGEST(E)
; check hash computed matches hash in the smt leaf
$ => A :MLOAD(hashContractTxDestAddr)
E :ASSERT, JMP(readCode)
然后处理字节码,此时交易calldata
存在CTX中的calldata
中。
readByteCode:
$ => E :MLOAD(contractHashId) ; hash index
$ => A :MLOAD(txDestAddr)
; check next byte exist on the bytecode
$ => B :MLOAD(bytecodeLength)
B - PC - 1 :JMPN(defaultOpCode) ; no bytecode treated as 0x00
PC => HASHPOS
$ => RR :HASHP1(E)
$${eventLog(onOpcode(RR))}
PC + 1 => PC :JMP(@mapping_opcodes + RR)
调用预编译合约
目前仅支持 ECRECOVER
和 IDENTITY
两个预编译合约。
selectorPrecompiled:
A - 2 :JMPN(funcECRECOVER)
A - 3 :JMPN(callContract) ;:JMPN(SHA256)
A - 4 :JMPN(callContract) ;:JMPN(RIPEMD160)
A - 5 :JMPN(IDENTITY)
A - 6 :JMPN(callContract) ;:JMPN(MODEXP)
A - 7 :JMPN(callContract) ;:JMPN(ECADD)
A - 8 :JMPN(callContract) ;:JMPN(ECMUL)
A - 9 :JMPN(callContract) ;:JMPN(ECPAIRING)
A - 10 :JMPN(callContract) ;:JMPN(BLAKE2F)
Gas 处理
检查计算要退回的 gas
, 主要是计算
;; compute maximum gas to refund
handleGas:
0 => A
$ => B :MLOAD(gasRefund), JMPZ(refundGas)
$ => A :MLOAD(txGasLimit)
A - GAS => A
; Div operation with Arith
A :MSTORE(arithA)
2 :MSTORE(arithB), CALL(divARITH); in: [arithA, arithB] out: [arithRes1: arithA/arithB, arithRes2: arithA%arithB]
$ => A :MLOAD(arithRes1)
A - B :JMPN(refundGas)
B => A
计算退回的 gas
,并添加到用户地址的余额中。
;; add remaining gas to transaction origin
refundGas:
GAS + A => GAS
GAS => A
$ => B :MLOAD(txGasPrice)
;Mul operation with Arith
A :MSTORE(arithA)
B :MSTORE(arithB), CALL(mulARITH)
$ => D :MLOAD(arithRes1)
$ => A :MLOAD(txSrcOriginAddr)
0 => B,C ; balance key smt
$ => A :SLOAD ; Original Balance in A
; Add operation with Arith
A :MSTORE(arithA)
D :MSTORE(arithB), CALL(addARITH)
$ => D :MLOAD(arithRes1)
$ => A :MLOAD(txSrcOriginAddr)
0 => B,C ; balance key smt
$ => SR :SSTORE
将消耗掉的 gas
发送给sequencerAddr
, 更新其地址余额。
sendGasSeq:
$ => A :MLOAD(txGasLimit)
A - GAS => A
$ => B :MLOAD(txGasPrice)
; Mul operation with Arith
A :MSTORE(arithA)
B :MSTORE(arithB), CALL(mulARITH)
$ => D :MLOAD(arithRes1) ; value to pay the sequencer in D
$ => A :MLOAD(sequencerAddr)
0 => B,C ; Balance key smt
$ => A :SLOAD ; Original Balance in A
; Add operation with Arith
A :MSTORE(arithA)
D :MSTORE(arithB), CALL(addARITH)
$ => D :MLOAD(arithRes1)
$ => A :MLOAD(sequencerAddr)
0 => B,C ; balance key smt
$ => SR :SSTORE, JMP(processTxEnd)
更新系统合约数据
首先更新系统合约中txCount
个数,将其增加1 。然后保存当前交易处理之后的状态根到系统合约中。
updateSystemData:
; check keccak counters
$ => A :MLOAD(cntKeccakPreProcess)
%MAX_CNT_KECCAK_F - CNT_KECCAK_F - A - 1:JMPN(outOfCountersKeccak)
; Get last tx count
%LAST_TX_STORAGE_POS => C
%ADDRESS_SYSTEM => A
%SMT_KEY_SC_STORAGE => B
$ => D :MLOAD(txCount)
;Update last tx Count at system storage
D + 1 => D :MSTORE(txCount)
$ => SR :SSTORE
;Update state root mapping
D => A
32 => D
0 => HASHPOS ; A new hash with position 0 is started
$ => E :MLOAD(lastHashKIdUsed)
E + 1 => E :MSTORE(lastHashKIdUsed)
A :HASHK(E)
%STATE_ROOT_STORAGE_POS :HASHK(E)
HASHPOS :HASHKLEN(E)
$ => C :HASHKDIGEST(E)
%ADDRESS_SYSTEM => A
SR => D
$ => SR :SSTORE, RETURN
最后通过txLoop
处理下一笔交易 。
Batch
计算
首先从ADDRESS_GLOBAL_EXIT_ROOT_MANAGER_L2
读取 newLocalExitRoot
;
然后根据batchHashPos
和batchL2DataLength
比较验证所有交易大小是否一致;
计算: batchHashData = keccak( transactions )
计算:newAccInputHash =keccak(oldAccInputHash, batchHashData, globalExitRoot, timestamp, sequencerAddr)
;; Get local exit root
; Read 'localExitRoot' variable from GLOBAL_EXIT_ROOT_MANAGER_L2 and store
; it to the 'newLocalExitRoot' input
%ADDRESS_GLOBAL_EXIT_ROOT_MANAGER_L2 => A
%SMT_KEY_SC_STORAGE => B
%LOCAL_EXIT_ROOT_STORAGE_POS => C
$ => A :SLOAD
A :MSTORE(newLocalExitRoot)
;; Transactions size verification
; Ensure bytes added to compute the 'batchHashData' matches the number of bytes loaded from input
$ => A :MLOAD(batchHashPos)
$ :MLOAD(batchL2DataLength), ASSERT
;; Compute 'batchHashData'
A => HASHPOS
$ => E :MLOAD(batchHashDataId)
HASHPOS :HASHKLEN(E)
$ => A :HASHKDIGEST(E)
A :MSTORE(batchHashData)
;; Compute 'newAccInputHash'
0 => HASHPOS
32 => D
$ => A :MLOAD(oldAccInputHash)
A :HASHK(0)
$ => A :MLOAD(batchHashData)
A :HASHK(0)
$ => A :MLOAD(globalExitRoot)
A :HASHK(0)
8 => D
$ => A :MLOAD(timestamp)
A :HASHK(0)
20 => D
$ => A :MLOAD(sequencerAddr)
A :HASHK(0)
HASHPOS :HASHKLEN(0)
$ => C :HASHKDIGEST(0)
C :MSTORE(newAccInputHash)
$${eventLog(onFinishBatch)}
Finalized execution
; Set output registers
$ => D :MLOAD(newAccInputHash)
$ => E :MLOAD(newLocalExitRoot)
$ => PC :MLOAD(newNumBatch)
; Set registers to its initials values
$ => B :MLOAD(oldStateRoot)
$ => C :MLOAD(oldAccInputHash)
$ => SP :MLOAD(oldNumBatch)
$ => GAS :MLOAD(chainID)
finalizeExecution:
:JMP(finalWait)
参考
https://github.com/0xPolygonHermez/zkevm-rom
https://github.com/0xPolygonHermez/zkevm-proverjs
https://ethbook.abyteahead.com/ch4/rlp.html
https://mirror.xyz/xyyme.eth/GNVcUgKAOEiLyClKeqkmD35ctLu6_XomT3ZDIfV3tz8
https://mirror.xyz/xyyme.eth/6vqE2DRsMzlPNmh3kYiwTdMBj-9hanmxyDuTHM7tZDU
https://mirror.xyz/xyyme.eth/6vqE2DRsMzlPNmh3kYiwTdMBj-9hanmxyDuTHM7tZDU
网友评论