本文为对ZkSync链上合约代码的学习笔记。
合约部署
合约部署需要指定起始genesis
状态根hash, 指定network governor
。
/// @notice zkSync contract initialization. Can be external because Proxy contract intercepts illegal calls of this function.
/// @param initializationParameters Encoded representation of initialization parameters:
/// @dev _governanceAddress The address of Governance contract
/// @dev _verifierAddress The address of Verifier contract
/// @dev _genesisStateHash Genesis blocks (first block) state tree root hash
function initialize(bytes calldata initializationParameters) external {
initializeReentrancyGuard();
(address _governanceAddress, address _verifierAddress, bytes32 _genesisStateHash) =
abi.decode(initializationParameters, (address, address, bytes32));
verifier = Verifier(_verifierAddress); // 验证者合约
governance = Governance(_governanceAddress); //治理合约
// We need initial state hash because it is used in the commitment of the next block
StoredBlockInfo memory storedBlockZero =
StoredBlockInfo(0, 0, EMPTY_STRING_KECCAK, 0, _genesisStateHash, bytes32(0));
storedBlockHashes[0] = hashStoredBlockInfo(storedBlockZero); // 存储创世块的hash值
}
其中 storedBlockHashes
是个map映射,主要存储区块状态的hash值。
/// @dev Stored hashed StoredBlockInfo for some block number
mapping(uint32 => bytes32) internal storedBlockHashes;
治理
网络治理由ZKSync中的networkGovernor
合约实现,它能够:
-
改变验证者;
-
添加新的
token
; -
移植新的合约;
抗审查
ZKSync利用 Priority queue
和Exodus mode
机制进行抗审查,保证资金的可取回。
存款
用户可通过下方式完成存款:
-
发送ETH到智能合约中;
-
调用
depositERC20()
函数
存款创建一个 deposit priority request
请求,并发出 NewPriorityRequest(opType, pubData, expirationBlock)
事件,通知验证者必须将该请求包含在下一个块中。
/// @notice Saves priority request in storage
/// @dev Calculates expiration block for request, store this request and emit NewPriorityRequest event
/// @param _opType Rollup operation type
/// @param _pubData Operation pubdata
function addPriorityRequest(Operations.OpType _opType, bytes memory _pubData) internal {
// Expiration block is: current block number + priority expiration delta
uint64 expirationBlock = uint64(block.number + PRIORITY_EXPIRATION);
uint64 nextPriorityRequestId = firstPriorityRequestId + totalOpenPriorityRequests;
bytes20 hashedPubData = Utils.hashBytesToBytes20(_pubData);
// 添加到优化级队列中
priorityRequests[nextPriorityRequestId] = PriorityOperation({
hashedPubData: hashedPubData,
expirationBlock: expirationBlock,
opType: _opType
});
// 发出事件
emit NewPriorityRequest(msg.sender, nextPriorityRequestId, _opType, _pubData, uint256(expirationBlock));
totalOpenPriorityRequests++;
}
priorityRequests
是个map映射,存储优先队列中的操作。
/// @dev Priority Requests mapping (request id - operation)
/// @dev Contains op type, pubdata and expiration block of unsatisfied requests.
/// @dev Numbers are in order of requests receiving
mapping(uint64 => PriorityOperation) internal priorityRequests;
当验证者提交的块中包含电路操作deposit
的时候,创建deposit onchain operation
并 priority request
的验证保持一致。
/// @notice Checks that deposit is same as operation in priority queue
/// @param _deposit Deposit data
/// @param _priorityRequestId Operation's id in priority queue
function checkPriorityOperation(Operations.Deposit memory _deposit, uint64 _priorityRequestId) internal view {
Operations.OpType priorReqType = priorityRequests[_priorityRequestId].opType;
require(priorReqType == Operations.OpType.Deposit, "H"); // incorrect priority op type
bytes20 hashedPubdata = priorityRequests[_priorityRequestId].hashedPubData;
require(Operations.checkDepositInPriorityQueue(_deposit, hashedPubdata), "I");
}
若合约进行 Exodus
模式,块未验证,块中Deposit priority requesst
请求中的资金返还给用户root-chain balance
, 保证其可取回。
取款
Partial withdrawl
当块中包括partial_exit
电路操作的时候,创建withdraw onchain operation
, 若块验证通过,withdraw onchain operation
中的资金返回给用户的root-chain balance
.
function executeOneBlock(ExecuteBlockInfo memory _blockExecuteData, uint32 _executedBlockIdx) internal {
// Ensure block was committed
require(
hashStoredBlockInfo(_blockExecuteData.storedBlock) ==
storedBlockHashes[_blockExecuteData.storedBlock.blockNumber],
"exe10" // executing block should be committed
);
require(_blockExecuteData.storedBlock.blockNumber == totalBlocksExecuted + _executedBlockIdx + 1, "k"); // Execute blocks in order
bytes32 pendingOnchainOpsHash = EMPTY_STRING_KECCAK;
for (uint32 i = 0; i < _blockExecuteData.pendingOnchainOpsPubdata.length; ++i) {
bytes memory pubData = _blockExecuteData.pendingOnchainOpsPubdata[i];
Operations.OpType opType = Operations.OpType(uint8(pubData[0]));
if (opType == Operations.OpType.PartialExit) {
Operations.PartialExit memory op = Operations.readPartialExitPubdata(pubData);
withdrawOrStore(op.tokenId, op.owner, op.amount); //Partial exit 取款
} else if (opType == Operations.OpType.ForcedExit) {
Operations.ForcedExit memory op = Operations.readForcedExitPubdata(pubData);
withdrawOrStore(op.tokenId, op.target, op.amount); //ForcedExit 取款
} else if (opType == Operations.OpType.FullExit) {
Operations.FullExit memory op = Operations.readFullExitPubdata(pubData);
withdrawOrStore(op.tokenId, op.owner, op.amount); // FullExit 取款
} else {
revert("l"); // unsupported op in block execution
}
pendingOnchainOpsHash = Utils.concatHash(pendingOnchainOpsHash, pubData);
}
require(pendingOnchainOpsHash == _blockExecuteData.storedBlock.pendingOnchainOperationsHash, "m"); // incorrect onchain ops executed
}
用户可以从root-chain balance
通过调用 withdrawETH
或withdrawERC20
取回资金。
function withdrawETH(uint128 _amount) external nonReentrant {
registerWithdrawal(0, _amount, msg.sender);
(bool success, ) = msg.sender.call{value: _amount}("");
require(success, "D"); // ETH withdraw failed
}
Full exit
用户可以发起Full exit
操作,当它认为他的交易被验证者审查的时候。
用户发起交易,调用ZKSync合约的requestFullExit()
函数,其创建full exit priority request
请求,并发出NewPriorityRequest(serialId, opType, pubData,expirationBlock)
事件通知验证者必须将其包括在下一个块中。
function requestFullExit(uint32 _accountId, address _token) public nonReentrant {
requireActive();
require(_accountId <= MAX_ACCOUNT_ID, "e");
uint16 tokenId;
if (_token == address(0)) {
tokenId = 0;
} else {
tokenId = governance.validateTokenAddress(_token);
}
// Priority Queue request
Operations.FullExit memory op =
Operations.FullExit({
accountId: _accountId,
owner: msg.sender,
tokenId: tokenId,
amount: 0 // unknown at this point
});
bytes memory pubData = Operations.writeFullExitPubdataForPriorityQueue(op);
addPriorityRequest(Operations.OpType.FullExit, pubData);
// User must fill storage slot of balancesToWithdraw(msg.sender, tokenId) with nonzero value
// In this case operator should just overwrite this slot during confirming withdrawal
bytes22 packedBalanceKey = packAddressAndTokenId(msg.sender, tokenId);
pendingBalances[packedBalanceKey].gasReserveValue = FILLED_GAS_RESERVE_VALUE;
}
当验证者提交的块中包括full_exit
操作的时候,创建对应的withdraw onchain operation
, 验证其和priority request
中的请求保持一致。若块验证通过,withdraw onchain operation
中的资金返回给用户的root-chain balance
.
若ZKSync合约进入Exodus
模式,withdraw onchain operation
和fulll exit priority requests
直接丢弃。
Block commitment
只有验证者可以提交一个块。已提交但未验证的块由MAX_UNVERIFIED_BLOCKS
常量限制,当验证失败时候,保证有充足的gas 来 revert
所有提交的块。
//批量提交多个块
function commitBlocks(StoredBlockInfo memory _lastCommittedBlockData, CommitBlockInfo[] memory _newBlocksData)
external
nonReentrant
{
requireActive();
governance.requireActiveValidator(msg.sender); // 只有验证者可以提交
// Check that we commit blocks after last committed block
require(storedBlockHashes[totalBlocksCommitted] == hashStoredBlockInfo(_lastCommittedBlockData), "i"); // incorrect previous block data
for (uint32 i = 0; i < _newBlocksData.length; ++i) {
// 对每个块进行验证
_lastCommittedBlockData = commitOneBlock(_lastCommittedBlockData, _newBlocksData[i]);
totalCommittedPriorityRequests += _lastCommittedBlockData.priorityOperations;
storedBlockHashes[_lastCommittedBlockData.blockNumber] = hashStoredBlockInfo(_lastCommittedBlockData);
emit BlockCommit(_lastCommittedBlockData.blockNumber);
}
totalBlocksCommitted += uint32(_newBlocksData.length);
require(totalCommittedPriorityRequests <= totalOpenPriorityRequests, "j");
}
Block verification
任何人都可以验证已提交的块。
function proveBlocks(StoredBlockInfo[] memory _committedBlocks, ProofInput memory _proof) external nonReentrant {
uint32 currentTotalBlocksProven = totalBlocksProven;
for (uint256 i = 0; i < _committedBlocks.length; ++i) {
//保证和先前提交的块一致
require(hashStoredBlockInfo(_committedBlocks[i]) == storedBlockHashes[currentTotalBlocksProven + 1], "o1");
++currentTotalBlocksProven;
// 保证提交的块和proof的承诺一致
require(_proof.commitments[i] & INPUT_MASK == uint256(_committedBlocks[i].commitment) & INPUT_MASK, "o"); // incorrect block commitment in proof
}
//采用聚合证明进行验证
bool success =
verifier.verifyAggregatedBlockProof(
_proof.recursiveInput,
_proof.proof,
_proof.vkIndexes,
_proof.commitments,
_proof.subproofsLimbs
);
require(success, "p"); // Aggregated proof verification fail
require(currentTotalBlocksProven <= totalBlocksCommitted, "q");
totalBlocksProven = currentTotalBlocksProven;
}
Block execution
只有验证者执行已经提交的块。
function executeBlocks(ExecuteBlockInfo[] memory _blocksData) external nonReentrant {
requireActive();
governance.requireActiveValidator(msg.sender);
uint64 priorityRequestsExecuted = 0;
uint32 nBlocks = uint32(_blocksData.length);
for (uint32 i = 0; i < nBlocks; ++i) {
executeOneBlock(_blocksData[i], i);
priorityRequestsExecuted += _blocksData[i].storedBlock.priorityOperations;
emit BlockVerification(_blocksData[i].storedBlock.blockNumber);
}
firstPriorityRequestId += priorityRequestsExecuted;
totalCommittedPriorityRequests -= priorityRequestsExecuted;
totalOpenPriorityRequests -= priorityRequestsExecuted;
totalBlocksExecuted += nBlocks;
// 保证只有验证过的块才能执行
require(totalBlocksExecuted <= totalBlocksProven, "n"); // Can't execute blocks more then committed and proven currently.
}
Revert Blocks
验证者可以撤销未执行的块。
/// @notice Reverts unverified blocks
function revertBlocks(StoredBlockInfo[] memory _blocksToRevert) external nonReentrant {
governance.requireActiveValidator(msg.sender);
uint32 blocksCommitted = totalBlocksCommitted;
uint32 blocksToRevert = Utils.minU32(uint32(_blocksToRevert.length), blocksCommitted - totalBlocksExecuted);
uint64 revertedPriorityRequests = 0;
for (uint32 i = 0; i < blocksToRevert; ++i) {
StoredBlockInfo memory storedBlockInfo = _blocksToRevert[i];
require(storedBlockHashes[blocksCommitted] == hashStoredBlockInfo(storedBlockInfo), "r"); // incorrect stored block info
delete storedBlockHashes[blocksCommitted];
--blocksCommitted;
revertedPriorityRequests += storedBlockInfo.priorityOperations;
}
totalBlocksCommitted = blocksCommitted;
totalCommittedPriorityRequests -= revertedPriorityRequests;
if (totalBlocksCommitted < totalBlocksProven) {
totalBlocksProven = totalBlocksCommitted;
}
emit BlocksRevert(totalBlocksExecuted, blocksCommitted);
}
优先级队列
Priority queue
保证 deposit
和 full_exit
操作能够及时处理,并被包含在ZKSync的块中。并且对于full_exit
交易,保证用户总是能够取款。
NewPriorityRequest
为用户发送交易触发的事件,它的结构为:
-
serialId
: 优先请示的id -
opType
: 操作类型 -
pubData
: 请求的数据 -
expirationBlock
: 请求过期的块号, expirationBlock = block.number + 250, 有效期将近一个小时
当块在Exodus mode
中过期的时候,Deposit priority requests
资金返回给用户的root-chain
账户, 然后丢弃Deposit priority requests
。
fee
当用户发送priority request
请求的时候,必须支付额外的费用,费用从Deposit
或 Full Exit
中的Ether
中扣除,费用的计算方式为:fee = FEE_COEFF * (BASE_GAS + gasleft) * gasprice
, 其中:
-
FEE_COEFF
: 系数 -
BASE_GAS
: 基本的gas费用,为21000 -
gasleft
: 交易执行的剩余gas
-
gasprice
: gas价格
验证者职责
验证者必须订阅 NewPriorityRequest
事件,并且保持正确的顺序,将优先的交易打包到下一个块中。验证者需要尽快打包这些交易,否则会进入Exodus
模式。
Exodus 模式
若Request Queue
处理过慢,可能触发ZKSync合约进入 Exodus
模式,当以太坊目前的区块超过expirationBlock
的时候。
/// @notice Checks if Exodus mode must be entered. If true - enters exodus mode and emits ExodusMode event.
/// @dev Exodus mode must be entered in case of current ethereum block number is higher than the oldest
/// @dev of existed priority requests expiration block number.
/// @return bool flag that is true if the Exodus mode must be entered.
function activateExodusMode() public returns (bool) {
bool trigger =
block.number >= priorityRequests[firstPriorityRequestId].expirationBlock &&
priorityRequests[firstPriorityRequestId].expirationBlock != 0;
if ($$(EASY_EXODUS) || trigger) {
if (!exodusMode) {
exodusMode = true;
emit ExodusMode();
}
return true;
} else {
return false;
}
}
在Exodus
模式下,合约冻结所有的块处理,所有的用户必须退出,所有存在的提交的块将被摊销。
所有的用户可以通过调用exit()
合约函数提交SNARK
证明,显示其在最新状态下拥有资金。若验证通过,用户的所有资金退回它的onchain balance
中。
function performExodus(
StoredBlockInfo memory _storedBlockInfo,
address _owner,
uint32 _accountId,
uint16 _tokenId,
uint128 _amount,
uint256[] memory _proof
) external nonReentrant {
bytes22 packedBalanceKey = packAddressAndTokenId(_owner, _tokenId);
require(exodusMode, "s"); // must be in exodus mode
require(!performedExodus[_accountId][_tokenId], "t"); // already exited
require(storedBlockHashes[totalBlocksExecuted] == hashStoredBlockInfo(_storedBlockInfo), "u"); // incorrect sotred block info
bool proofCorrect =
verifier.verifyExitProof(_storedBlockInfo.stateHash, _accountId, _owner, _tokenId, _amount, _proof);
require(proofCorrect, "x");
increaseBalanceToWithdraw(packedBalanceKey, _amount);
performedExodus[_accountId][_tokenId] = true;
}
移植
迁移到新的合约比较简单,但必须允许用户可以选择性加入,或允许用户退出。
参考
https://github.com/matter-labs/zksync/blob/master/docs/contract.md
https://github.com/matter-labs/zksync/tree/master/contracts/contracts
网友评论