zkSync学习记录 2023.01.31

Hacker Dōjō Web3前沿技术 workshop文稿
资助金额:120 USDT
Bounty 链接:https://dorahacks.io/daobounty/167
内容贡献者:0x7ac61a
赏金发布:https://bscscan.com/tx/0x753e311fd7f80e8e89777b318daa6f97bb8ae7465ed5a60649e9624bffdda7bc
本项目由Hacker Dōjō 资助,文章转载请注明出处。
Telegram: @DoraDojo0
WeChat: @HackerDojo0
E-mail: hackerdojo0@gmail.com

zkSync学习

image

一、扩容方案

image

  1. Layer1扩容:对区块链本身的性能提升(共识优化,Sharding)
  2. Layer2扩容:不更改区块链本身,而是在链下计算,将结果上链
    1. Data Availability(DA):数据可用性
    2. State Validity(SV):状态有效性防止Layer2停止维护数据丢失导致资产锁死

Rollup

  • 保证Data Availability

    • Layer2的数据压缩后上传到Layer1,由Layer1保证DA
    • Validium:数据不上传到Layer1,由多个Validator托管
    • Volidation&zkPorter:用户可以选择数据是否上传到Layer1
      image
  • 保证State Validity:

    • Optimistic rollup:利用欺诈证明(fraud proofs),
    • ZK rollup:利用有效性证明(validity proofs)

zkPorter

zkPorter在zkSync 2.0版本中可用,zkPorter的引入意味着用户可以决定是否在链上或链下存储他们的数据。
zkSync在zkRollup和zkPorter方面采取的双账户方法将为用户提供一个全面的目的地来进行他们的活动,同时保留高度的安全保证。zkRollup和zkPorter两边的智能合约和账户将完全可以相互组合。
zkRollup和zkPorter都是使用zkp进行验证,只不过zkRollup数据可用性在链上保证,zkPorter数据可用性由“Guardians”这个PoS 网络维护,类似于其它的L1 PoS系统。
维护zkPorter账户的PoS系统有Guardians和validators

  • validators负责将zkSync 2.0上的交易分组打包,完成产生zkp的运算工作(类似于zkRollup里面的Prover)
  • Guardians是zkSync代币的持有者,Guardians必须以绝对多数(2/3)签署每个状态转换(一批交易)。如果任何Guardians作恶,他们就有失去抵押资产的风险。

image
涉及到zkPorter的安全性需要考虑到两种情况:

  • 如果超过了1/3的Guardians作恶,就不会有区块产生,因为破坏了绝对多数原则;
  • 如果2/3的守护者作恶,整个zkPorter状态就会被冻结,但这同样也会冻结他们的质押代币,所以发生的可能性较小;

image

三、zkSync

Overview

运行zkSync网络的节点需要能够执行以下操作:

  • 监控链上智能合约的操作(如存款)
  • 接受交易
  • 生成zkSync链块
  • 请求已执行区块的证明
  • 将数据发布到智能合约

image

  • Layer1
    • zkSync Smart Contract:部署在以太坊网络上的Solidity智能合约,用于管理用户balances并验证zkSync network操作的正确性。
  • Layer2
    • Mempool:收集交易(Layer1与Layer2的交易)
    • Block Proposer:将交易打包,发送给State Keeper执行
    • StateKeeper:更新ZkSync的状态,在世界状态更改后,发送给Block Committer生成证明需要的信息
    • Block Committer:负责持久化区块、并更新state
    • Plonk Proving System:生成一个区块的proof

Account表示

在zkSync没有独立生成新账户。zkSync的L2账户和L1账户一一对应,L1的私钥的ECDSA签名的结果会作为L2账户的私钥。
image

在Layer2中Account结构包括了以下几个字段:

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>,
}

zkSync的Token信息由Layer1的Governance.sol维护,通过addToken函数tokenGovernance可以为网络中添加一个新的token,每一个token都有一个对应的从0递增的tokenID,其中tokenID=0被保留,对应为eth。

zkSync交易类型

zkSync目前支持如下交易类型(操作类型):

/// zkSync network operation.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ZkSyncOp {
    Deposit(Box<DepositOp>),
    Transfer(Box<TransferOp>),
    /// Transfer to new operation is represented by `Transfer` transaction,
    /// same as `Transfer` operation. The difference is that for `TransferToNew` operation
    /// recipient account doesn't exist and has to be created.
    TransferToNew(Box<TransferToNewOp>),
    Withdraw(Box<WithdrawOp>),
    WithdrawNFT(Box<WithdrawNFTOp>),
    #[doc(hidden)]
    Close(Box<CloseOp>),
    FullExit(Box<FullExitOp>),
    ChangePubKeyOffchain(Box<ChangePubKeyOp>),
    ForcedExit(Box<ForcedExitOp>),
    MintNFTOp(Box<MintNFTOp>),
    /// `NoOp` operation cannot be directly created, but it's used to fill the block capacity.
    Noop(NoopOp),
    Swap(Box<SwapOp>),
}

zksync实现了一个priority queue(优先通道),所有L1 user发起的deposit和withdraw(full exit)都可以独立于L2的交易优先完成:

/// A set of L1 priority operations supported by the zkSync network.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ZkSyncPriorityOp {
    Deposit(Deposit),
    FullExit(FullExit),
}

Deposit/FullExit

  1. 用户参与Layer2,可以通过调用Layer1合约中的depositERC20函数,将代币转入合约
  2. 将用户请求encode成pubdata
  3. 放入priorityRequests队列中
  4. 触发事件,后续会被server监听到
/// @notice Deposit ERC20 token to Layer 2 - transfer ERC20 tokens from user into contract, validate it, register deposit
/// @param _token Token address
/// @param _amount Token amount
/// @param _zkSyncAddress Receiver Layer 2 address
function depositERC20(
      IERC20 _token,
      uint104 _amount,
      address _zkSyncAddress
    ) external nonReentrant

2.
        // Priority Queue request
        Operations.Deposit memory op = Operations.Deposit({
            accountId: 0, // unknown at this point
            owner: _owner,
            tokenId: _tokenId,
            amount: _amount
        });
        bytes memory pubData = Operations.writeDepositPubdataForPriorityQueue(op);
3.
priorityRequests[nextPriorityRequestId] = PriorityOperation({
            hashedPubData: hashedPubData,
            expirationBlock: expirationBlock,
            opType: _opType
        });
4.
emit NewPriorityRequest(msg.sender, nextPriorityRequestId, _opType, _pubData, uint256(expirationBlock));

Core application

在run_core的过程中,主要zkSync主要有以下几个子模块:(core/bin/server/src/main.rs->run_server->run_core):

  • Ethereum Watcher:监控链上操作
  • zkSync state keeper, 执行和封装块。
  • mempool:组织传入的交易
  • block proposer, 为state keeper提交block
  • committer, 将待处理和已完成的块存储到数据库中
  • API server
    let task_futures = vec![
        eth_watch_task,
        state_keeper_task,
        root_hash_calculator_task,
        committer_task,
        token_handler_task,
        register_factory_task,
        tx_event_emitter_task,
        mempool_block_handler_task,
        mempool_tx_handler_task,
        private_api_task,
    ];

image

Ethereum Watcher

监控链上操作的模块

#[derive(Debug, Default, Clone)]
pub struct ETHState {
    /// The last block of the Ethereum network known to the Ethereum watcher.
    last_ethereum_block: u64,
    /// The previous Ethereum block successfully processed by the watcher.
    /// Keeping track of it is required to be able to poll the node for
    /// the same range multiple times, e.g. in case it didn't return all
    /// priority operations received by the contract.
    last_ethereum_block_backup: u64,
    /// Serial id of the next priority operation Ethereum watcher should process.
    next_priority_op_id: SerialId, 
    /// Queue of priority operations that are accepted by Ethereum network,
    /// but not yet have enough confirmations to be processed by zkSync.
    ///
    /// Note that since these operations do not have enough confirmations,
    /// they may be not executed in the future, so this list is approximate.
    unconfirmed_queue: Vec<PriorityOp>,
    /// Keys in this HashMap are numbers of blocks with `PriorityOp`.
    /// Queue of priority operations that passed the confirmation
    /// threshold and are waiting to be executed.
    priority_queue: HashMap<u64, ReceivedPriorityOp>,
    /// List of tokens that have been added to the contract.
    new_tokens: Vec<NewTokenEvent>,
    /// List of events denoting registered factories for NFT withdrawing
    register_nft_factory_events: Vec<RegisterNFTFactoryEvent>,
}
  1. 启动时拉取链上最新的块高block
  2. restore_state_from_eth:更新Layer1交易事件,设置EthState
  3. 循环执行poll_eth_node:
    1. process_new_blocks,更新ETHState

Mempool

  1. mempool handler收集交易
MempoolTransactionRequest::NewTx(tx, resp) => {
    let tx_add_result = self.add_tx(*tx).await;
    resp.send(tx_add_result).unwrap_or_default();
}
MempoolTransactionRequest::NewTxsBatch(txs, eth_signatures, resp) => {
    let tx_add_result = self.add_batch(txs, eth_signatures).await;
    resp.send(tx_add_result).unwrap_or_default();
}
MempoolTransactionRequest::NewPriorityOps(ops, confirmed, resp) => {
    let tx_add_result = self.add_priority_ops(ops, confirmed).await;
    resp.send(tx_add_result).unwrap_or_default();
}
  1. 接收MempoolBlocksRequest::GetBlock请求,生成proposed block

State Keeper

  1. 初始化(restore_from_db)
    1. load_account_tree
    2. load_nft_tokens
    3. load_root_hash_jobs
    4. load_pending_block
    5. load_reverted_blocks
    6. ZkSyncState => init_params
  2. propose_new_block,发送请求至mempool,获取提交的block
  3. execute_proposed_block
    1. 执行proposed_block中的priority_ops(Layer1交易)(proposed_block.priority_ops
      1. 执行后放入executed_ops中
    2. 执行proposed_block的tx(proposed_block.txs
      1. 执行后放入executed_ops中
    3. 如果
      1. pending_block没有剩余空间或者处理的轮数(每次自增1)已经达到上限,执行seal_pending_block(store_pending_block->CommitRequest::PendingBlock)
      2. 否则调用store_pending_block

Committer

  1. save_pending_block
    1. 把pending_block插入db中
    2. commit_state_update:
      1. account的更新,更新至db
      2. 更新committed nonce值
  2. seal_incomplete_block
    1. commit_state_update
    2. save_incomplete_block:把不完整的区块存入数据库
    3. save_block_metadata:向block_metadata插入记录
  3. poll_for_new_proofs_task:定时任务,生成聚合操作
pub async fn create_aggregated_operations_storage(
    storage: &mut StorageProcessor<'_>,
    config: &ChainConfig,
) -> anyhow::Result<()> {
    while create_aggregated_commits_storage(storage, config).await? {}
    while create_aggregated_prover_task_storage(storage, config).await? {}
    while create_aggregated_publish_proof_operation_storage(storage).await? {}
    while create_aggregated_execute_operation_storage(storage, config).await? {}

    Ok(())
}

上面4种操作最后会生成aggregate_operations、eth_unprocessed_aggregated_ops(除了create_aggregated_prover_task_storage)数据,后被Ethereum Sender处理。

Ethereum Sender

  1. load_new_operations:从数据库中获取传入的ops并将其添加到tx_queue。
    1. add_operation_to_queue:根据类型将tx放到CommitBlocks
      1. AggregatedActionType::CommitBlocks
      2. AggregatedActionType::PublishProofBlocksOnchain
      3. AggregatedActionType::ExecuteBlocks
  2. proceed_next_operations
    1. 从tx_queue出队列交易,转为eth交易,并存储在表eth_operations中
    2. 将operations放在ongoing_ops,向L1发送签名交易
  3. gas_adjuster.keep_updated:维护最新的gas price limit

L1相关合约

  1. commit block:保存StoredBlockInfo在链上
/// @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 Data needed to commit new block
    struct CommitBlockInfo {
        bytes32 newStateHash;
        bytes publicData;
        uint256 timestamp;
        OnchainOperationData[] onchainOperations;
        uint32 blockNumber;
        uint32 feeAccount;
    }
  • 依次执行每个commitBlockInfo,主要调用commitOneBlock函数
    • 检查时间戳
    • 执行collectOnchainOps(_newBlock)函数
    • 对区块创建区块的commitment
    • 最后返回StoredBlockInfo

StoredBlockInfo结构体:

    /// @Rollup block stored data
    /// @member blockNumber Rollup block number
    /// @member priorityOperations Number of priority operations processed
    /// @member pendingOnchainOperationsHash Hash of all operations that must be processed after verify
    /// @member timestamp Rollup block timestamp, have the same format as Ethereum block constant
    /// @member stateHash Root hash of the rollup state
    /// @member commitment Verified input for the zkSync circuit
    struct StoredBlockInfo {
        uint32 blockNumber;
        uint64 priorityOperations;
        bytes32 pendingOnchainOperationsHash;
        uint256 timestamp;
        bytes32 stateHash;
        bytes32 commitment;
    }

Prover

Plonk证明系统涉及到三个功能模块:Block Committer,Prover Server以及Prover。
image

PlonK

一种零知识证明的方法需要具备如下三个性质:

  • 完备性 (Completeness):若所要证之事为真,则诚实的证明者能说服诚实验证者
  • 可靠性 (Soundness):若命题为假,则作弊证明者仅有极小机会能说服诚实验证者该事为真。
  • 零知识性 (Zero-knowledgeness):若命题为真,则验证者除此之外,过程中没有得悉任何其他信息。

PlonK的工作原理概述:如果用户能够对抛物线上的某一点连续给出正确答案,那么就可以确信他知道这条抛物线函数是什么,因为每一轮成功猜出正确答案的概率会越来越低。这个过程被转换为电路所表示的约束,进行验证的生成:
image

协议流程可以简单描述为:

  • 根据电路生成三个多项式,分别代表这电路的左输入,右输入,输出;
  • 利用置换校验协议,去证明复制约束关系成立;
  • 校验门的约束关系成立。
  • Plonk中将约束主要分为门约束(运算和输入)、线约束(复制约束),然后分别对这两类约束构建零知识证明和验证。

这部分在源码中对应在circuit中。

witness生成

从电路的角度,每一种交易可以分割成多个Operation。一个区块中的交易分割成多个Operation。
image

例如对于swap可以分为6个Operation,其他操作参考官方文档:
image

在证明了这些Operation的正确性后,潜在证明了区块中包含的交易的正确性。

#[derive(Clone, Debug)]
pub struct Operation<E: RescueEngine> {
    pub new_root: Option<E::Fr>,
    pub tx_type: Option<E::Fr>,
    pub chunk: Option<E::Fr>,
    pub pubdata_chunk: Option<E::Fr>,
    pub signer_pub_key_packed: Vec<Option<bool>>,
    pub first_sig_msg: Option<E::Fr>,
    pub second_sig_msg: Option<E::Fr>,
    pub third_sig_msg: Option<E::Fr>,
    pub signature_data: SignatureData,
    pub args: OperationArguments<E>,
    pub lhs: OperationBranch<E>,
    pub rhs: OperationBranch<E>,
}

witness生成:(core/bin/zksync_witness_generator/src/witness_geneator.rs

  1. prepare_witness_and_save_it
    1. load_account_tree:加载区块的账户树,zksync电路使用用的账户树
      1. load_committed_state:读取已经committed的account map
      2. load_state_diff:加载verified_block与committed_block之间的AccountUpdates,包含account_balance_updates、account_creates、account_pubkey_updates、mint_nft_updates
      3. circuit_account_tree.insert:往空的SparseMerkleTree插入account map的元素,得到committed状态的zksync电路账户树
      4. 比较持久化的committed block的root hash与计算得到的电路账户树root hash是否一致
    2. build_block_witness:基于circuit_account_tree生成WitnessBuilder,再由WitnessBuilder转为ProverData(用于生成proof)
      1. WitnessBuilder::new,基于账户树及区块信息构建Builder
      2. 遍历block_transactions,针对不同交易类型,先把交易应用到账户树,再生成operations、pub_data、offset_commitment
      3. 基于已有信息更新WitnessBuilder
    3. store_witness:将WitnessBuilder转为ProverData并序列化,存入block_witness表

L1相关合约

合约中验证proof:

    /// @notice Blocks commitment verification.
    /// @notice Only verifies block commitments without any other processing
    function proveBlocks(StoredBlockInfo[] memory _committedBlocks, ProofInput memory _proof) external nonReentrant
		...
		bool success = verifier.verifyAggregatedBlockProof(
            _proof.recursiveInput,
            _proof.proof,
            _proof.vkIndexes,
            _proof.commitments,
            _proof.subproofsLimbs
        );

zkEvm

ZK证明需要将它们证明的所有计算语句转换为非常特定的格式——一种“代数电路”,然后可以将其编译成 STARK或SNARK。
在zkSync中自定义了VM与对应的转译器,zkSync正在同时开发两个针对 zkEVM 的编译器前端:Yul和Zinc。Yul是一种中间 Solidity 表示,可以编译为不同后端的字节码。Zinc是我们用于智能合约和通用零知识证明电路的基于Rust的语言。
image

image

Reference: