L2 - 深入理解zkSync電路

買賣虛擬貨幣
zkSync的電路設計很有意思,值得好好學習。眾所周知,一個區塊中會打包不同的交易,如果只是針對一個個交易進行電路的證明,電路大小會變化。zkSync將交易切割成更小的“通用電路“。一個區塊中包含固定的”通用電路“,間接支援多個交易。

zkSync的原始碼目錄下的docs/circuit.md簡述了zksync的電路實現的原理以及各個操作的結構。zkSync的Layer2交易證明基於PLONK證明系統,功能模組如下:

1. 電路表示

zksync的電路由FranklinCircuit結構表示。整個電路的邏輯實現在core/circuit/src/circuit.rs。

pub struct FranklinCircuit<'a, E: RescueEngine + JubjubEngine> {
    pub rescue_params: &'a <E as RescueEngine>::Params,
    pub jubjub_params: &'a <E as JubjubEngine>::Params,
    /// The old root of the tree
    pub old_root: Option<E::Fr>,
    pub initial_used_subtree_root: Option<E::Fr>,

    pub block_number: Option<E::Fr>,
    pub validator_address: Option<E::Fr>,

    pub pub_data_commitment: Option<E::Fr>,
    pub operations: Vec<Operation<E>>,

    pub validator_balances: Vec<Option<E::Fr>>,
    pub validator_audit_path: Vec<Option<E::Fr>>,
    pub validator_account: AccountWitness<E>,
}

FranklinCircuit包括了Rescue以及Jubjub引數,老的狀態根,Validator的地址/餘額以及在Merkle樹上的路徑資訊,pub data的commitment資訊(區塊中所有的Operation對應的pub data對應的commitment),以及各個Operation的資訊。簡單的說,FranklinCircuit包括了,當前狀態(狀態根),交易資訊(pub data以及Operation)以及Validator需要收取的手續費。

Pub Data

每一筆Layer2的交易,會發布完備的資訊到Layer1。這些資訊就稱為Pub Data。每種交易型別有不同長度。為了固定證明電路,Pub Data被切割成多個塊(Chunk)。

Operation

從電路的角度,每一種交易,“分割”成多個Operation。一個區塊中的交易分割成多個Operation。

在證明了這些Operation的正確性後,潛在證明了區塊中包含的交易的正確性。電路中的Operation定義在core/circuit/src/operation.rs中 :

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

new_root是新的狀態根,tx_typ指交易型別,chunk是交易多個chunk的編號,pubdata_chunk是交易對應的pubdata的chunk編號。signer_pub_key_packed是交易簽名的pub key。整個交易(msg)分成最大三段。每段的位元組長度不超過Fr的bit個數。signature_data是簽名資訊。args是交易的引數資訊。lhs/rhs分別指的是狀態操作的分支資訊。

交易的引數資訊由OperationArguments資料結構表示。你可以把Operation中除了OperationArugments的部分想象成一種操作的固定資訊。OperationArguments的部分是一種操作變化的部分。舉個例子,Transfer的交易,Operation中的交易型別,pub data對應的chunk是固定的。而轉賬金額,費用,以太地址針對不同的Transfer交易而不同。

pub struct OperationArguments<E: RescueEngine> {
    pub a: Option<E::Fr>,
    pub b: Option<E::Fr>,
    pub amount_packed: Option<E::Fr>,
    pub full_amount: Option<E::Fr>,
    pub fee: Option<E::Fr>,
    pub new_pub_key_hash: Option<E::Fr>,
    pub eth_address: Option<E::Fr>,
    pub pub_nonce: Option<E::Fr>,
}

amount_packed是交易的金額,fee是交易的費用。其他也比較清晰。講講a,b。在每個Operation的證明電路中,提供了一個大於等於的判斷。也就是說,判定a是否大於b。針對不同的交易,a和b的含義不同。以Transfer為例,a代表傳送方的餘額,b代表傳送的金額加上費用。Transfer的證明電路證明傳送方有足夠的餘額支付交易。以Deposit為例,a代表金額,b設定為0。Deposit的證明電路證明Deposit的交易金額必須大於0。

某個狀態分支由OperationBranch資料結構表示:

pub struct OperationBranch<E: RescueEngine> {
    pub address: Option<E::Fr>,
    pub token: Option<E::Fr>,

    pub witness: OperationBranchWitness<E>,
}

address表示zksync內部的賬戶編號,token是代幣對應的編號,witness是分支對應的證明資訊。

2.0 電路核心邏輯

熟悉Bellman或者對R1CS電路搭建有經驗的小夥伴都知道,一切從synthesize函式開始:

fn synthesize<CS: ConstraintSystem<E>>(self, cs: &mut CS) -> Result<(), SynthesisError> {
在檢視具體邏輯之前,必須清晰的是,電路證明的是一個Block的合法性。一個Block包括了多個交易(Tx和PriorityOp),每個交易對應的多個Operation。

申請固定變數

申請固定為0的變數。兩種形式:AllocatedNum以及CircuitElement。CircuitElement是抽象的資料結構,實現電路的最基本的元素:

      let zero = AllocatedNum::alloc(cs.namespace(|| "allocate element equal to zero"), || {
            Ok(E::Fr::zero())
        })?;

        zero.assert_zero(cs.namespace(|| "enforce zero on the zero element"))?;

        // we only need this for consistency of first operation
        let zero_circuit_element = CircuitElement::unsafe_empty_of_some_length(zero.clone(), 256);

可以看出,CircuitElement就是AllocatedNum的封裝,提供了更多變數的資訊(bits資訊和長度)。

pub struct CircuitElement<E: Engine> {
    number: AllocatedNum<E>,
    bits_le: Vec<Boolean>,
    length: usize,
}

建立PreviousData

PreviousData現在主要是前一個Operation對應的各種資訊資料。

struct PreviousData<E: RescueEngine> {
    op_data: AllocatedOperationData<E>,
}     

let mut prev = PreviousData {
    op_data: AllocatedOperationData::empty_from_zero(zero.clone())?,
}

從AllocatedOperationData的定義可以清晰的看出,AllocatedOperationData包含了前一個Operation的各種資訊對應的變數。

pub struct AllocatedOperationData<E: Engine> {
    pub amount_packed: CircuitElement<E>,
    pub fee_packed: CircuitElement<E>,
    pub amount_unpacked: CircuitElement<E>,
    pub full_amount: CircuitElement<E>,
    pub fee: CircuitElement<E>,
    pub first_sig_msg: CircuitElement<E>,
    pub second_sig_msg: CircuitElement<E>,
    pub third_sig_msg: CircuitElement<E>,
    pub new_pubkey_hash: CircuitElement<E>,
    pub eth_address: CircuitElement<E>,
    pub pub_nonce: CircuitElement<E>,
    pub a: CircuitElement<E>,
    pub b: CircuitElement<E>,
}

建立Pub Data Commitment

區塊資訊對應的Pub Data從Layer2會提交到Layer1。區塊證明的資訊,顯然要驗證是否和提交的Pub Data一致。

// vector of pub_data_bits that will be aggregated during block processing
let mut block_pub_data_bits = vec![];       

let public_data_commitment =
    AllocatedNum::alloc(cs.namespace(|| "public_data_commitment"), || {
    self.pub_data_commitment.grab()
    })?;
public_data_commitment.inputize(cs.namespace(|| "inputize pub_data"))?;
public_data_commitment也是整個電路唯一的公開輸入。

確認狀態根

整個狀態是由賬戶和Token資訊組成的兩層Merkle樹。賬戶是以追加方式一個個新增。賬戶資訊比較少的情況下,存在大量的空節點。

    let old_root = AllocatedNum::alloc(cs.namespace(|| "old_root"), || self.old_root.grab())?;
        let mut rolling_root = {
            let initial_used_subtree_root =
                AllocatedNum::alloc(cs.namespace(|| "initial_used_subtree_root"), || {
                    self.initial_used_subtree_root.grab()
                })?;
            let old_root_from_subroot = continue_leftmost_subroot_to_root(
                cs.namespace(|| "continue initial_used_subtree root to old_root"),
                &initial_used_subtree_root,
                params::used_account_subtree_depth(),
                params::account_tree_depth(),
                self.rescue_params,
            )?;
            // ensure that old root contains initial_root
            cs.enforce(
                || "old_root contains initial_used_subtree_root",
                |lc| lc + old_root_from_subroot.get_variable(),
                |lc| lc + CS::one(),
                |lc| lc + old_root.get_variable(),
            );

            initial_used_subtree_root
        };

rolling_root為包括了所有賬戶的最小樹的樹根。從rolling_root到最終的root,需要再和一些空節點計算hash。

確定Chunk的狀態

next_chunk_num為下一個chunk的編號對應的變數。區塊中的第一個交易的第一個Operation的第一個chunk編號為0。

let mut next_chunk_number = zero.clone();

let mut allocated_chunk_data: AllocatedChunkData<E> = AllocatedChunkData {
    is_chunk_last: Boolean::constant(false),
    is_chunk_first: Boolean::constant(false),
    chunk_number: zero_circuit_element.get_number(),
    tx_type: zero_circuit_element,
};

allocated_chunk_data表明當前的chunk的狀態:是否是第一個或者最後一個狀態,對應的chunk編號,以及交易型別。注意,這些都是電路的變數。

確定Pub Data是否一致

從證明電路的角度看,每個交易都劃分為不同的Operation。針對一個交易的不同Operation,要證明處理的是同一個Pub Data。pubdata_holder就是實現這樣的目的。因為一個區塊可能包含不同的Operation,所以,pubdata_holder列舉包含所有的Operation的可能性。

let mut pubdata_holder = {
    let mut data = vec![vec![]; DIFFERENT_TRANSACTIONS_TYPE_NUMBER];

    data[NoopOp::OP_CODE as usize] = vec![]; // No-op allocated constant pubdata
    data[DepositOp::OP_CODE as usize] = vec![zero.clone(); 2];
    data[TransferOp::OP_CODE as usize] = vec![zero.clone(); 1];
    data[TransferToNewOp::OP_CODE as usize] = vec![zero.clone(); 2];
    data[WithdrawOp::OP_CODE as usize] = vec![zero.clone(); 2];
    data[FullExitOp::OP_CODE as usize] = vec![zero.clone(); 2];
    data[ChangePubKeyOp::OP_CODE as usize] = vec![zero.clone(); 2];

    // this operation is disabled for now
    // data[CloseOp::OP_CODE as usize] = vec![];

    data
};

初始化Fee對應變數

在一個區塊中的交易,可能轉賬或者提取任何Token。所以,一個區塊對應的Fees是一個陣列。Fee初始都為0。

let mut fees = vec![];
let fees_len = params::number_of_processable_tokens();
for _ in 0..fees_len {
    fees.push(zero_circuit_element.get_number());
}

Operation處理

整個證明電路包括了一個區塊中的所有Operation的證明。

        // Main cycle that processes operations:
        for (i, operation) in self.operations.iter().enumerate() {

        }

每一個Operation的處理都可以分成幾個小步驟:

1. 驗證chunk編號

驗證當前的chunk是不是和next_chunk_number一致。並且建立更多變數,表示當前的狀態:交易型別,是不是第一個chunk,是不是最後一個chunk,當前的chunk編號。

            let (next_chunk, chunk_data) = self.verify_correct_chunking(
                &operation,
                &next_chunk_number,
                cs.namespace(|| "verify_correct_chunking"),
            )?;

2. 收集pub data

將每個Operation對應的chunk data收集到block pub data中。

            let operation_pub_data_chunk = CircuitElement::from_fe_with_known_length(
                cs.namespace(|| "operation_pub_data_chunk"),
                || operation.clone().pubdata_chunk.grab(),
                params::CHUNK_BIT_WIDTH,
            )?;
            block_pub_data_bits.extend(operation_pub_data_chunk.get_bits_le());

3. 分支選擇

邏輯上,每個交易會改變一些branch,所謂的branch,就是狀態樹上的分支(賬戶和Token)。

            let lhs =
                AllocatedOperationBranch::from_witness(cs.namespace(|| "lhs"), &operation.lhs)?;
            let rhs =
                AllocatedOperationBranch::from_witness(cs.namespace(|| "rhs"), &operation.rhs)?;
            let mut current_branch = self.select_branch(
                cs.namespace(|| "select appropriate branch"),
                &lhs,
                &rhs,
                operation,
                &allocated_chunk_data,
            )?;

目前的交易型別,最多改動兩個分支。也就是Operation裡面定義的lhs和rhs(左分支和右分支)。針對每個Operation,選定一個branch(需要在Operation後進行一定的更新,Nonce,Balance等等)。當然,對於一個交易的所有Operation,要能覆蓋所有的branch。相關的邏輯由select_branch確定。

如果是Deposit交易,永遠選擇lhs。如果是其他交易,第一個選擇lhs,其他選擇rhs。

4. 狀態根檢查

在選定branch後,需要確定當前狀態和之前計算的rolling是否一致。

            // calculate root for given account data
            let (state_root, is_account_empty, _subtree_root) = check_account_data(
                cs.namespace(|| "calculate account root"),
                &current_branch,
                params::used_account_subtree_depth(),
                self.rescue_params,
            )?;

            // ensure root hash of state before applying operation is correct
            cs.enforce(
                || "root state before applying operation is valid",
                |lc| lc + state_root.get_variable(),
                |lc| lc + CS::one(),
                |lc| lc + rolling_root.get_variable(),
            );

5. 執行Operation

execute_op執行某個具體的Operation。邏輯上,execute_op電路中包括了所有的可能的Operation的操作。只是在執行某個具體的Operation,只有其中一個有效。

            self.execute_op(
                cs.namespace(|| "execute_op"),
                &mut current_branch,
                &lhs,
                &rhs,
                &operation,
                &allocated_chunk_data,
                &is_account_empty,
                &operation_pub_data_chunk.get_number(),
                // &subtree_root, // Close disable
                &mut last_token_id,
                &mut fees,
                &mut prev,
                &mut pubdata_holder,
                &zero,
            )?;

execute_op檢查如下的條件是否滿足:

1/ 左右的branch的token,是否一致?
2/ Op Data和Prev是否一致?
3/ 交易簽名是否正確?
4/ a是否滿足大於等於b?(a/b,請檢視Operation)
5/ 是否是支援的交易中的一種?
6/ 更新Fee

因為一個Opeartion的電路需要邏輯上支援不同交易。不同交易不一樣的邏輯由單獨的電路處理。目前支援支援7種操作,以transer為例,理解一下具體的業務電路的實現:

fn transfer<CS: ConstraintSystem<E>>(
        &self,
        mut cs: CS,
        cur: &mut AllocatedOperationBranch<E>,
        lhs: &AllocatedOperationBranch<E>,
        rhs: &AllocatedOperationBranch<E>,
        chunk_data: &AllocatedChunkData<E>,
        is_a_geq_b: &Boolean,
        is_account_empty: &Boolean,
        op_data: &AllocatedOperationData<E>,
        signer_key: &AllocatedSignerPubkey<E>,
        ext_pubdata_chunk: &AllocatedNum<E>,
        is_sig_verified: &Boolean,
        pubdata_holder: &mut Vec<AllocatedNum<E>>,
    ) -> Result<Boolean, SynthesisError> {

1/ 確保pub data一致
2/ 確保是Transer型別
3/ 確保a/b的意義一致(a代表餘額,b代表轉賬金額加上手續費)
4/ 兩個branch的資訊是否有效?
5/ 更新當前的branch資訊
6. 確認並更新狀態根

在執行完當前Operation後,當前的branch的相關資訊(nonce,balance等等)都會更新。需要更新一下對應的狀態根,因為下一個Operation需要在新的狀態根上更新。注意,下一個Operation提供的branch witness資訊是在上一個Operation更新的基礎上產生的。電路只需要證明新的Root正確即可。這個也是原因,整個證明電路並沒有維護整個狀態樹的原因。

            let (new_state_root, _, _) = check_account_data(
                cs.namespace(|| "calculate new account root"),
                &current_branch,
                params::used_account_subtree_depth(),
                self.rescue_params,
            )?;

            rolling_root = new_state_root;

檢視Chunk是否完整

在Block中的所有Operation執行完成後,is_chunk_last應為true。也就是說,Block中的最後一個交易的Operation是完整的。

        cs.enforce(
            || "ensure last chunk of the block is a last chunk of corresponding transaction",
            |_| {
                allocated_chunk_data
                    .is_chunk_last
                    .lc(CS::one(), E::Fr::one())
            },
            |lc| lc + CS::one(),
            |lc| lc + CS::one(),
        );

計算交易費後的狀態

在支付交易費給Validator後,重新計算Validator的Balance樹的樹根,以及新的狀態根。

        //apply fees to operator balances
        for i in 0..fees_len {
            validator_balances_processable_tokens[i] = allocate_sum(
                cs.namespace(|| format!("validator balance number i {}", i)),
                &validator_balances_processable_tokens[i],
                &fees[i],
            )?;
        }

        // calculate operator's balance_tree root hash from whole tree representation
        let new_operator_balance_root = calculate_balances_root_from_left_tree_values(
            cs.namespace(|| "calculate_root_from_full_representation_fees after"),
            &validator_balances_processable_tokens,
            params::balance_tree_depth(),
            self.rescue_params,
        )?;

        let mut operator_account_data = vec![];
        let new_operator_state_root = {
            let balance_root = CircuitElement::from_number(
                cs.namespace(|| "new_operator_balance_root_ce"),
                new_operator_balance_root,
            )?;
            calc_account_state_tree_root(
                cs.namespace(|| "new_operator_state_root"),
                &balance_root,
                &self.rescue_params,
            )?
        }; 關鍵詞:     

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;