ZkSync架构及源码初探
Note. 本篇将简单介绍ZkSync开源项目整体架构、核心模块的功能及整体源码分析。希望能与大家一起探索现在比较火的Layer2开源项目,后续能够基于ZkSync开发出永续合约等应用。
1. ZkSync整体架构
什么是ZK-Rollup(ZKR)?
引用自Matter Labs的介绍:
In a ZK-Rollup, operator(s) must generate a succinct Zero-Knowledge Proof (SNARK) for every state transition, which is verified by the Rollup contract on the mainchain. This SNARK proves that there exists a series of transactions, correctly signed by owners, which update the account balances in the correct way, and which lead from the old Merkle root to the new one. It is thus impossible for the operators to commit an invalid or manipulated state.
这是对ZKR的总结,理解这段话也就比较好理解接下来ZkSync项目模块设计的目的及流程了。
ZkSync架构图
图 1 ZkSync架构图
2. 核心模块分析
2.1 初始化
genesis_init: 创建二层创世块和初始化账户,将初始化token持久化到DB
- create_genesis_block:创建二层创世块
- 构造2个特殊账户,fee_account、nft存储账户并更新nft特殊的balance
- 添加genesis_tokens
2.2 Mempool
内存交易池,接收api发过来的交易请求;接收Block Proposer的GetBlock请求,组装Proposed Block;接受Committer的UpdateNonces请求,修改账户nonce
- restore_from_db(加载已经持久化但尚未执行的交易至pending queue,加载account_nonces、account_ids) => load_committed_state
- load_committed_state(加载committed状态的账户状态) => 先load_verify_state,再load_state_diff(last_commited_block,即blocks表最大的一个块高),apply_update(verify_state,diff)
- load_verify_state:加载已经确认的账户状态,及该状态所处的区块高度
- load_state_diff: 会读取相应区块内的account_balance_updates、account_creates、account_pubkey_updates信息,生成AccountUpdate记录
- apply_update:将AccountUpdate应用到Account,最后更新AccountMap
另外会接收来自block proposer发过来的请求(线程间使用channel通讯),接收GetBlock请求,生成proposed block(由在块最大交易数量所允许情况下的一层和二层交易组成,其中一层交易来自Ethereum Watcher,二层交易来自mempool的transaction queue),block proposer会再将block发给state keeper执行。
接收来自Committer发过来的UpdateNonces请求,修改mempool_state内账户的nonce
2.3 Block Proposer
负责block的生成,定期从mempool拉取ProposedBlock,再发送给ZkSyncStateKeeper执行
current_priority_op_number: 当前未处理的一层操作编号?初始化的时候从statekeeper获取 mempool_requests: 给mempool发送请求的channel statekeeper_requests: 给statekeeper发送请求的channel
定期commit_new_tx_mini_batch:
- 获取pending_block_timestamp,向mempool拉取Proposed Block
- 将Proposed Block发送至State Keeper,ExecuteMiniBlock
2.4 ZkSyncStateKeeper
负责更新ZkSync的状态,接收来自Block Proposer的ProposedBlock并执行,并向Committer发送生成block或者pending block请求
1.初始化加载ZkSyncStateInitParams初始状态,从数据库加载AccountTree、LastCommittedBlockNumber、UnProcessedPriorityOp,读取PendingBlock(有的话) 2.如果存在PendingBlock,说明仍然有一些状态没更新到数据库,所以需要在当前状态执行一遍PendingBlock的Tx、PriorityOp,得到最新的状态 3.等待接收和处理block proposer发过来的请求(线程间使用channel通讯),如ExecuteMiniBlock
接收到来自Block Proposer的ExecuteMiniBlock,执行execute_proposed_block。
- 先执行来自Proposed Block内的一层交易,再执行二层交易,执行交易会更新账户树等状态。
- 如果pending_block没有剩余空间或者处理的轮数(每次自增1)已经达到上限,就会seal_pending_block。seal_pending_block会修改State,将pending block转为full block,固化pending block,会向Committer发送CommitRequest::Block。
- 还没达到seal_pending_block条件,会调用store_pending_block,store_pending_block修改State,并向Committer发送CommitRequest::PendingBlock,将中间结果中间持久化到db,所以执行的交易就不会丢失。
2.5 Committer
负责持久化block、updates记录
handle_new_commit_task:生成full block,该方法接收到CommitBlock Request的时候会根据CommitRequest的block类型进行commit_block(Block)或者save_pending_block(PendingBlock)
- commit_block:
- commit_state_update: 将account map的更新持久化到db,但这些更新还没被验证,根据block里面的交易类型,向表account_updates、account_balance_updates、account_pubkey_updates、mint_nft_updates插入或者删除数据;
- save_block:
- save_block_transactions:
- set_account_type:
- store_executed_tx(二层交易):从mempool_txs表移除记录,向executed_transactions表插入记录
- store_executed_priority_op(一层交易):向executed_priority_operations表插入记录
- 删除pending_block表中的记录
- 向blocks表插入数据
- save_block_transactions:
- save_block_metadata: 向block_metadata插入记录
- 向mempool发送UpdateNonces
- save_pending_block:
- save_pending_block:
- 向pending_block表插入记录
- save_block_transactions:操作同save_block的子过程一样
- commit_state_update:操作同commit_block的子过程一样
- save_pending_block:
2.6 AggregatedCommitter
定时生成aggregate_operations,再由Ethereum Sender读取发送到L1
poll_for_new_proofs_task:定时任务,生成聚合操作,一共包含4种类型的聚合操作(插入aggregated_operations、commit_aggregated_blocks_binding、execute_aggregated_blocks_binding、eth_unprocessed_aggregaed_ops表,eth_sender后续会读取表aggregated_operations、eth_unprocessed_aggregaed_ops数据,向L1发送交易)
- create_aggregated_commits_storage:从aggregate_operations读取last_aggregate_committed_block_number,然后加载该block与最新block之间的blocks,生成聚合操作
- create_aggregated_prover_task_storage:读取last_aggregate_create_proof_block与last_committed_block之间已经生成proof的区块(proofs表有数据),生成聚合操作
- create_aggregated_publish_proof_operation_storage:读取last_aggregate_publish_proof_block与last_aggregate_create_proof_block之间aggregated_proofs,生成聚合操作
- create_aggregated_execute_operation_storage:读取last_aggregate_executed_block与last_aggregate_publish_proof_block之间的区块,生成聚合操作
- 以上4种操作,最后都会调用store_aggregated_action生成aggregate_operations、eth_unprocessed_aggregated_ops(除了create_aggregated_prover_task_storage)数据,后续被Ethereum Sender处理
2.7 Ethereum Sender
读取AggregatedCommiter生成的操作,向L1发送交易
启动时:
- restore_unprocessed_operations:从aggregate_operations读取unconfirmed和尚未发向L1的聚合操作,向表eth_unprocessed_aggregated_ops插入记录
- load_unconfirmed_operations:从eth_operations读取unconfirmed的操作,每个操作包含一些待发送的以太坊交易,放置于ongoing_ops
- remove_unprocessed_operations:从eth_unprocessed_aggregated_ops移除记录(因为该表只是一个临时表,前一步已经将数据放置在ongoing_ops?)
- load_stats:读取表eth_parameters,包含last_committed_block、last_verified_block、last_executed_block信息
- TxQueueBuilder:初始化各种交易类型的队列
定期:
- load_new_operations:
- 调用load_unprocessed_operations:从表eth_unprocessed_aggregated_ops读取操作ID,再读取aggregate_operations表读取操作数据,从eth_unprocessed_aggregated_ops删除ID(把操作标记为成功处理),再添加到tx_queue,根据类型,会根据聚合操作类型,放到CommitBlocks、PublishProofBlocksOnChain、ExecuteBlocks的队列(CreateProofBlocks类型除外),不同的聚合操作类型通过operation_to_raw_tx将转为不同的zksync合约交易类型
- proceed_next_operations:
- 从tx_queue出队列交易,转为eth交易,并存储在表eth_operations中(为了获取操作ID及更新nonce,操作eth_parameters表),同时还会将operations放在ongoing_ops(为了能够在发送L1失败的时候,后续能够重试),再向L1发送签名交易,此时数据库事务结束
- 清理ongoing_ops,过滤掉已经完成op和处理剩余的其他op(比如为阻塞的交易发送一个补偿交易)
- gas_adjuster.keep_updated:维护最新的gas price limit
CommitBlocks聚合操作:调用ZkSync合约的commitBlocks方法(最低优先级) PublishProofBlocksOnchain聚合操作:调用ZkSync合约的proveBlocks方法(次高优先级) ExecuteBlocks聚合操作:调用ZkSync合约的executeBlocks方法(最高优先级)
/// @notice Commit block
/// @notice 1. Checks onchain operations, timestamp.
/// @notice 2. Store block commitments
function commitBlocks(StoredBlockInfo memory _lastCommittedBlockData, CommitBlockInfo[] memory _newBlocksData)
external
nonReentrant
{
// ...
}
/// @notice Blocks commitment verification.
/// @notice Only verifies block commitments without any other processing
function proveBlocks(StoredBlockInfo[] memory _committedBlocks, ProofInput memory _proof) external nonReentrant {
// ...
}
/// @notice Execute blocks, completing priority operations and processing withdrawals.
/// @notice 1. Processes all pending operations (Send Exits, Complete priority requests)
/// @notice 2. Finalizes block on Ethereum
function executeBlocks(ExecuteBlockInfo[] memory _blocksData) external nonReentrant {
// ...
}
2.8 Ethereum Watcher
监听链上事件,获取Priority Op,为mempool出块提供一层交易
NewPriorityRequest:一层操作请求 NewToken:新增token NFTFactoryRegisteredCreator:NFT工厂?
run:主流程
- 启动时拉取链上最新的块高block
- restore_state_from_eth(block):拉取从event安全确认块到最新块之间的1层交易事件(NewPriorityRequest)、新增token(NewToken)、新增NFT(NFTFactoryRegisteredCreator),设置当前EthState
- 定期poll_eth_node:获取最新块与上一次EthState之间的block_difference(上述3种增量),合并增量,设置当前EthState
- 接受来自core api的请求:EthWatchRequest::GetPriorityQueueOps、EthWatchReques::GetUnconfirmedOps等等,从EthState里读取数据并返回
图 2 Watcher模块图
2.9 Witness Generator
一旦有新block,就会生成witness,作为proof零知识证明的原材料
load_last_verified_block => get_last_verified_block => get_last_block_by_aggregated_action(AggregatedActionType::ExecuteBlocks,None) ,获取最后一个已经被验证执行的block number,刚开始没有ExecuteBlocks时为0 多个witness_generators按自己的block number间隔去查询尚未生成witness的block(NoWitness,即block_witness表尚无该block的数据),计算当前block的proof。通过prepare_witness_and_save_it,插入block_witness表,后续prover server会读取并生成update_prover_job。
prepare_witness_and_save_it:
- load_account_tree:加载区块的账户树,zksync电路使用用的账户树
- load_committed_state:读取已经committed(不一定要verified)的account map
- load_verified_state:读取聚合操作类型为ExecuteBlocks(proof已经在1层被确认,即应用到链上状态)的缩影想到的最大block及对应的account map
- load_state_diff:加载verified_block与committed_block之间的AccountUpdates,包含account_balance_updates、account_creates、account_pubkey_updates、mint_nft_updates
- apply_updates: 对verified状态的account map应用上面的updates,得到committed状态的account map
- circuit_account_tree.insert:往空的SparseMerkleTree插入account map的元素,得到committed状态的zksync电路账户树
- assert_eq!(storage_block.new_root_hash,circuit_account_tree.root_hash()):比较持久化的committed block的root hash与计算得到的电路账户树root hash是否一致
- load_committed_state:读取已经committed(不一定要verified)的account map
- build_block_witness: 基于circuit_account_tree生成WitnessBuilder,再由WitnessBuilder转为ProverData(用于生成proof)
- WitnessBuilder::new,基于账户树及区块信息构建Builder
- 遍历block_transactions,针对不同交易类型,先把交易应用到账户树,再生成operations、pub_data、offset_commitment
- 如ZkSyncOp::Deposit,transfer_witness = TransferWitness::apply_tx(&mut witness_accum.account_tree, &transfer);
- 计算input,input = SigDataInput::from_transfer_op(&transfer) 这里会涉及到用户的签名, transfer_operations = calculate_operations(input),计算得到operations
- 计算transfer_witness.get_pubdata()
- 计算offset_commitment,transfer_witness.get_offset_commitment_data()
- 基于已有信息更新WitnessBuilder
- store_witness:将WitnessBuilder转为ProverData并序列化,存入block_witness表
pub type AccountMap = zksync_crypto::fav::FnvHashMap<AccountId,Account>; AccountId为Layer2数据库的账号id,Account结构体如下
/// zkSync network account.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Account {
/// Hash of the account public key used to authorize operations for this account.
/// Once account is created (e.g. by `Transfer` or `Deposit` operation), account owner
/// has to set its public key hash via `ChangePubKey` transaction, so the server will be
/// able to verify owner's identity when processing account transactions.
pub pub_key_hash: PubKeyHash,
/// Address of the account. Directly corresponds to the L1 address.
pub address: Address,
balances: HashMap<TokenId, BigUintSerdeWrapper>,
/// Current nonce of the account. All the transactions require nonce field to be set in
/// order to not allow double spend, and the nonce must increment by one after each operation.
pub nonce: Nonce,
pub minted_nfts: HashMap<TokenId, NFT>,
}
2.10 Prover Server
间隔性生成SingleProof、AggregatedProof证明任务队列,并提供内部api与prover交互
update_prover_job_queue_loop,间隔性update_prover_job_queue update_prover_job_queue : 先尝试添加单证任务,再尝试添加聚合证明任务 单证
- load_last_block_prover_job_queue(SingleProof),从prover_job_queue表读取单证任务队列最大的last_block块高,为空时返回聚合类型为CreateProofBlocks的最大to_block块高
- load_witness,从block_witness读取序列化后的ProverData,并构造成JobRequestData
- 将序列化后的JobRequest存入prover_job_queue 聚合证明
- load_last_block_prover_job_queue(AggregatedProof),从prover_job_queue表读取聚合证明任务队列最大的last_block块高,为空时返回聚合类型为PublishProofBlocksOnchain的最大to_block块高
- 从aggregate_operations读取CreateProofBlocks类型且为上述块高的聚合操作的arguments,里面记录了lastCommittedBlock及到commitBlock之间的blocks信息
- 遍历其间的blocks,从proofs表读取每个block的证明并拼接在一起,格式为【proof1,block1_size】…【proofxx,blockxx_size】,最后构造成JobRequestData
- 将序列化后的JobRequest存入prover_job_queue
提供内部api(主要是查询和修改表prover_job_queue):
- /status,查询prover_job状态
- /get_job,获取prover_job
- /working_on,标记某个prover正在处理该prover_job
- /publish,标记某个prover_job已经完成,并插入记录至表proofs、aggregated_proofs
- /stopped,标记某个prover_job的状态为idle
- /api/interanl/prover/replicas,计算需要有多少个prover server
prover_job状态有3类:
- Idle => 0
- InProgress => 1
- Done => 2
2.11 Prover
为executed block生成proof,当有新的块并且有对应的witness的时候,会拉取update_prover_job_queue开始为其生成单证、聚合证明,生成完毕通知Prover Server持久化证明
plonk_step_by_step_prover
prover_work_cycle:
- get_job,从prover server的update_prover_job_queue获取job data
- compute_proof_no_blocking,生成proof
- publish,标记prover_job已经完成,并将proof记录至proofs或者aggregated_proofs
3. 参考资料
[1] zkSync is Live! Bringing Trustless, Scalable Payments to Ethereum