Zkopru uses Baby Jubjub curve for its Elliptic Curve Cryptography. The arithmeric is defined in the reference paper written by Barry Whitehat and Jordi Baylina.
Let
\[
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
\]
and, \(\mathbb{F}_p\) be the finite field which modular is \(p\).
Let \(p\) and \(\mathbb{F}_p\) be defined in [1.1].
The Montgomerry form of the Baby Jubjub curve
\[
E_M: v^2 = u^3 + 168698u^2 + u
\]
The order of \(E_M\) is
\[
n = 21888242871839275222246405745257275088614511777268538073601725287587578984328 \\
= 8 \times r
\]
where \(r = 2736030358979909402780800718157159386076813972158567259200215660948447373041\) is a prime number.
Let \(\mathbb{F}_r\) be the finite field which modular is \(r\).
Denote \(\mathbb{G}\) the subgroup of points of order \(r\), that is
\[
\mathbb{G} = \{\mathbf{P} \in E(\mathbb{F}_p) | r\mathbf{P} = \mathbf{O}\}
\]
where \(\mathbf{O}\) is the infinite zero.
Let \(p\) and \(\mathbb{F}_p\) be defined in [1.1].
Let \(E_M\), \(n\) be defined in [1.2].
The Edward form of the curve \(E\) that is birationally equivalent to \(E_M\) defined over \(\mathbb{F}_p\)
\[
E: x^2 + y^2 = 1 + dx^2y^2
\]
where
\(d = 9706598848417545097372247223557719406784115219466060233080913168975159366771\).
If \((u, v)\) is a Baby Jubjub point in the Montgomerry form, its equivalent Edward form
\[
(x,y) = \left( \frac{1 + y}{1 - y}, \frac{1 + y}{(1 - y)x} \right)
\]
If \((x, y)\) is a Baby Jubjub point in the Edward form, its equivalent Montgomerry form
\[
(u,v) = \left( \frac{u}{v}, \frac{u - 1}{u + 1} \right)
\]
\(\mathsf{poseidon_n}\) is a poseidon hash function which consumes \(n\) number of inputs.
Let
\[
y = \mathsf{poseidon_n}([x_1, x_2, ..., x_n]) \\
\]
Then,
\[
x_i, y \in \mathbb{F}_p\\
\]
Where \(i\) is an integer and \(0 < i \le n\)
The number of rounds of \(\mathsf{poseidon_n}\) depends on \(n\), and its recommended value is defined in the table2 and table 4 of the Poseidon hash research paper
\(n\) | \(t\) | \(R_F\) | \(R_P\) |
---|---|---|---|
2 | 3 | 8 | 57 |
3 | 4 | 8 | 56 |
4 | 5 | 8 | 60 |
The implementation that Zkopru uses are
\(\mathbf{G}\) is a constant generator point on the babyjubjub curve and its value is \((995203441582195749578291179787384436505546430278305826713579947235728471134,\\ 5472060717959818805561601436314318772137091100104008585924551046643952123905)\).
Let \(\mathbf{G}\) be as defined in [1.5]
\(\mathbf{B} = 8 \cdot \mathbf{G}\).
\(\mathbf{B}\) is the base point for the EdDSA in Zkopru that has 8 for its cofactor and 2736030358979909402780800718157159386076813972158567259200215660948447373041 for its order.
\(s\) is the 32 bytes private key of the account. For the seamless user experience, Zkopru uses Ethereum account's private key or deterministically derived key from it for its corresponding Zkopru account's private seed key.
Let \(r, \mathbb{F}_r\) be defined in [1.2].
Let \(s\) be defined in [1.7].
\(a \in \mathbb{F}_r\) is the scalar multiplier that is generated from the private seed key \(s\) by the RFC8032 5.1.5 Key Generation protocol with blake512 hash function.
Let \(\mathbf{B}\) be defined in [1.6]
Let \(a\) be defined in [1.8]
\(\mathbf{A} \in \mathbb{G}\) is the EdDSA public key that is \(\mathbf{A} = a \cdot \mathbf{B}\).
Let \(\mathbb{F}_r\) be defined in [1.2].
Let \(s\) be defined in [1.7].
\(v \in \mathbb{F}_r\) is used for a viewing key and a nullifier seed where
\[ v = keccak256(s)\ mod\ r \]
Let \(\mathsf{poseidon_n}\) be defined in [1.4]
Let \(\mathbf{A}\) be defined in [1.9]
Let \(v\) be defined in [1.10]
\(\mathsf{Pub_{sk}} \in \mathbb{F}_p\) is the public spending key of Zkopru that is
\[
\mathsf{Pub_{sk}} = \mathsf{poseidon_3}(x, y, v)
\]
where \((x,y) = A\).
Let \(\mathbb{G}\) be defined in [1.2].
Let \(\mathbf{B}\) be defined in [1.6].
Let \(v\) be defined in [1.10].
\(\mathbf{V} \in \mathbb{G}\) is the public viewing key that is
\[
\mathbf{V} = v \cdot \mathbf{B}
\]
Let \(s\) be defined in [1.7]
Let \(\mathsf{Pub_{sk}}\) be defined in [1.11]
Let \(\mathbf{V}\) be defined in [1.12]
\(\mathcal{Z}\) is the Zkopru account address from the private key \(s\).
Point encode/decode protocol: https://tools.ietf.org/html/rfc8032#section-5.1.2
Let \(\mathbb{F}_p\) be defined in [1.1]
Let \(\mathsf{poseidon}_5\) be defined in [1.4]
Let \(s\) be defined in [1.7]
Let \(\mathbf{A}\) be defined in [1.9]
Then \((\mathbf{R}, S)\) is the EdDSA signature of the EdDSA public vector \(\mathbf{A}\) for about the message \(m \in \mathbb{F}_p\).
\[
\mathsf{EdDSA}(\mathbf{A}, m) = (\mathbf{R}, S)
\]
It follows the RFC8032 with blake512 hash and \(\mathsf{poseidon}_5\) hash function.
for the private key hashing and the prefix generation.
Zkopru is using iden3's EdDSA implementation here
Zkopru uses groth16 and BN254(= BN128) curve for the SNARK pairing. As defined in 2.1. Bilinear Groups of the groth16 paper, \(\mathbb{G_1}\) and \(\mathbb{G_2}\) are the group of order \(q = 21888242871839275222246405745257275088696311157297823662689037894645226208583\).
Zkopru note (denoted \(\mathsf{N}\)) is a tuple \((S, \mathsf{asset}, \mathsf{salt} )\) where
\(S\) is defined in [1.11].
\(\mathsf{asset}\) is defined in [2.2]
\(\mathsf{salt}\): is a random 16 bytes salt data.
Let \(p\) be defined in [1.1]
Let \(\mathsf{poseidon_n}\) be defined in [1.4]
\(\mathsf{asset}\) is a tuple of \(\mathsf{(v_{eth}, addr_{token}, v_{erc20}, v_{nft})}\) where
NFTs with ID 0 cannot be deposited on the \(\mathsf{ZkopruContract}\).
:: :
Let \(\mathsf{asset, (v_{eth}, addr_{token}, v_{erc20}, v_{nft})}\) be defined in [2.2]
Then, the hash of the asset is computed by:
\[
\mathsf{hash(\mathsf{asset})} = \mathsf{poseidon_4}(\mathsf{v_{eth}, addr_{token}, v_{erc20}, v_{nft}})
\]
Let \(\mathsf{poseidon_n}\) be defined in [1.4]
Let \(S, \mathsf{N, asset, salt}\) be defined in [2.1]
Let \(\mathsf{hash(\mathsf{asset})}\) be defined in [2.3]
Then, the hash value of the note is computed by:
\[
\mathsf{hash}(\mathsf{N}) = \mathsf{poseidon_3}(S, \mathsf{salt}, \mathsf{hash(\mathsf{asset})})
\]
Please note that the information written in \(\mathcal{Calligraphic}\) typeface is known to everyone while information written in \(\mathrm{Roman}\) typeface is only known to the prover.
Let \(\mathsf{N}\) be defined in [2.1].
Let \(\mathsf{hash}(\mathsf{N})\) be defined in [2.4].
The transaction output of Zkopru
\[
\mathrm{Out} = (\mathsf{N}, t, \mathcal{N})
\]
where \(t\) is the type of the output and \(\mathcal{N}\) is the public signals of the output that is
\(\mathcal{N} = \mathsf{(to, v'_{eth}, addr'_{token}, v'_{erc20}, v'_{nft}, fee_{L1})} \\ \ = \left\{\begin{array}{lr} (0, 0, 0, 0, 0, 0) & \text{for } t = 0 & \text{(utxo)}\\ \mathsf{(recipient, v_{eth}, addr_{token}, v_{erc20}, v_{nft}, fee_{caller})} & \text{for } t = 1 & \text{(withdrawal)}\\ \mathsf{(dest, v_{eth}, addr_{token}, v_{erc20}, v_{nft}, fee_{migration})} & \text{for } t = 2 & \text{(migration)} \end{array}\right\}\)
Then, the public outflow data is defined as
\[ \mathcal{Out} = (\mathsf{hash}(\mathsf{N}), t, \mathcal{N}) \]
Let \(\mathsf{t}\) be defined in [3.1.1]
\(\mathrm{U}\) is a UTXO type raw transaction output \(\mathrm{Out}\) which \(t = 0\) while \(\mathcal{U}\) is its corresponding public output \(\mathcal{Out}\).
Then, \(\mathsf{hash}(\mathrm{U}) = \mathsf{hash(N)}\).
Let \(\mathsf{t}\) be defined in [3.1.1]
\(\mathrm{W}\) is a withdrawal type raw transaction output \(\mathrm{Out}\) which \(t = 1\) while \(\mathcal{W}\) is its corresponding public output \(\mathcal{Out}\).
Then,
\(\mathsf{hash}(\mathcal{W}) = keccak256(\mathsf{encodePacked(\mathcal{W})})\)
where \(\mathsf{encodePacked(\mathcal{W})}\) is 200 bytes data that is encoded with big-endian by the following table:
Position | Size | Data |
---|---|---|
0-32 | 32 bytes | \(\mathsf{hash(N)}\) |
32-52 | 20 bytes | \(\mathcal{N}.\mathsf{to}\) |
52-84 | 32 bytes | \(\mathcal{N}.\mathsf{v_{eth}}\) |
84-104 | 20 bytes | \(\mathcal{N}.\mathsf{addr_{token}}\) |
104-136 | 32 bytes | \(\mathcal{N}.\mathsf{v_{erc20}}\) |
136-168 | 32 bytes | \(\mathcal{N}.\mathsf{v_{nft}}\) |
168-200 | 32 bytes | \(\mathcal{N}.\mathsf{fee_{L1}}\) |
Let \(\mathsf{t}\) be defined in [3.1.1]
\(\mathrm{M}\) is a migration type raw transaction output \(\mathrm{Out}\) which \(t = 2\) and \(\mathsf{hash}(\mathrm{M}) = \mathsf{hash(N)}\).
And \(\mathcal{M}\) is its corresponding public output \(\mathcal{Out}\)
Let \(\mathrm{U}\) be defined in [3.1.2].
Let \(\mathsf{Tree_\mathsf{utxo}}\) be defined in [5.3.1].
\(\mathsf{pos}(\mathrm{U})\) is the leaf index of \(\mathrm{U}\) in the UTXO Merkle tree \(\mathsf{Tree}_\mathsf{utxo}\).
Let \(v\) be defined in [1.10]
Let \(\mathrm{U}\) be defined in [3.1.2].
Let \(\mathsf{pos}(\mathrm{U})\) be defined in [3.1.5].
Then the nullifier that prevents double spending is computed by:
\[ \mathsf{nullifier}(\mathrm{U}) = \mathsf{poseidon_2}(v, \mathsf{pos}(\mathrm{U})) \]
Let \(\mathsf{hash}(\mathsf{N})\) be defined in [2.4].
Let \(\mathrm{U}\) be defined in [3.1.1].
Let \(\mathsf{pos}(\mathrm{U})\) be defined in [3.1.5].
Let \(\mathsf{Tree_\mathsf{utxo}}\) be defined in [5.3.1].
Let \(\mathsf{root_{utxo}}^{(n)}\) be defined in [6.1.2].
Let the root of \(\mathsf{Tree}_\mathsf{utxo}\) at the \(n\)-th layer 2 block be denoted \(\mathsf{root_{utxo}}^{(n)}\).
\(\mathsf{Sib}_\mathsf{utxo}^{(n)}(\mathrm{U})\) is the set of all sibling node data of UTXO \(\mathrm{U}\) at the \(n\)-th layer 2 block.
Then \(\mathsf{Merkle}^{(n)}_\mathsf{utxo}(\mathrm{U}) = (\mathsf{hash}(\mathrm{U}), \mathsf{pos}(\mathrm{U}), \mathsf{root_{utxo}}^{(n)}, \mathsf{Sib}_\mathrm{U}^{(n)}(\mathrm{U}))\) that satisfies the Merkle tree inclusion proof.
Let \(\mathsf{Merkle}_\mathsf{utxo}^{(n)}\) be defined in [3.1.7]
The confidential data of the \(i\)-th input note
\[
\mathrm{In}_i = (\mathrm{U}, (\mathbf{A}, v), \mathbf{R}, S, \mathsf{Merkle}_\mathsf{utxo}^{(n)}(\mathrm{U}), \mathsf{nullifier}(\mathrm{U}))
\]
where
Then, its public inflow data is defined as
\[
\mathcal{In} = (\mathsf{nullifier}(\mathrm{U}), \mathsf{root_{utxo}}^{(n)})
\]
Let \(\mathrm{U}\) be defined in [3.1.2]
Let \(\mathrm{W}\) be defined in [3.1.3]
Let \(\mathrm{M}\) be defined in [3.1.4]
Let \(\mathrm{In}\) and \(\mathcal{In}\) be defined in [3.1.8]
Let \(\mathrm{Out}\) and \(\mathcal{Out}\) be defined in [3.1.1]
Then, \(\mathrm{Flow}\) is the set of raw inflow and outflow:
\[
\mathrm{Flow} = (\mathrm{[In_1, ..., In_m], [Out_1, ..., Out_n]})
\]
while \(\mathcal{Flow}\) is the hidings of \(\mathrm{Flow}\) which corresponds to \(\mathrm{Flow}\)
\[
\mathcal{Flow} = (\mathcal{[In_1, ..., In_m], [Out_1, ..., Out_n]})
\]
Let \(\mathrm{Flow}\) be defined in [3.1.9]
Then, a raw transaction \(\mathrm{Tx}\) is
\[ \mathrm{Tx} = (\mathrm{Flow}, f, \mathsf{swap}) \]
where \(f\) is the fee for the layer 2 block proposer and \(\mathsf{swap}\) is the desired atomic swap output from its paired transaction as defined in X.Y.Z.
\(\mathsf{C}_{(x,y)}\) is a Zkopru circuit that consumes \(x\) inputs and emits \(y\) outputs.
Let \(\mathsf{C}_{(x,y)}\) be defined in [3.2.1].
\(\mathsf{zPK}_{(x,y)}\) is the proving key for the circuit \(\mathsf{C}_{(x,y)}\) and should be setup by the multi party computation.
Let \(\mathsf{C}_{(x,y)}\) be defined in [3.2.1].
Let \(\mathsf{zPK}_{(x,y)}\) be defined in [3.2.2].
\(\mathsf{zVK}_{(x,y)}\) is the verifying key for the circuit \(\mathsf{C}_{(x,y)}\) that corresponds to the proving key \(\mathsf{zPK}_{(x,y)}\).
Let \(\mathrm{In}\) and \(\mathcal{In}\) be defined in [3.1.8]
Let \(\mathrm{Flow}, \mathcal{Flow}\) be defined in [3.1.9]
Let \(f, \mathsf{swap}\) be defined in [3.1.10]
Let \(\mathsf{C}_{(x,y)}\) be defined in [3.2.1].
Then a prover can generate a witness
\[
\mathsf{w} \leftarrow \mathsf{witness}(\mathsf{C}_{(m,n)}, \mathrm{Flow}, \mathcal{Flow}, f, \mathsf{swap})
\]
when\((\mathsf{C}_{(m,n)}, \mathrm{Flow}, \mathcal{Flow}, f, \mathsf{swap})\) satisfies the conditions:
Let \(\mathrm{In}\) be the part of \(\mathrm{Flow}\) as defined in [3.1.9]
Each \(\mathrm{In}\) should satisfy the conditions in [3.1.6]
Let \(\mathrm{In}\) be the part of \(\mathrm{Flow}\) as defined in [3.1.9]
Each \(\mathrm{In}\) should satisfy the conditions in [3.1.8]
Let \(\mathrm{U}\) be the part of \(\mathrm{In}\) as defined in [3.1.8]
Each \(\mathrm{U}\) should satisfy the conditions in [3.1.7]
Let \(\mathrm{Out}\) be the part of \(\mathrm{Flow}\) as defined in [3.1.9]
Let \(\mathcal{Out}\) be the part of \(\mathcal{Flow}\) as defined in [3.1.9]
Then,
\(\mathcal{Out}_i\) and \(\mathrm{Out}_i\) should satisfty the condition [3.1.1]
Let \(\mathrm{In}\) be the part of \(\mathrm{Flow}\) as defined in [3.1.9]
Let \(\mathrm{Out}\) be the part of \(\mathrm{Flow}\) as defined in [3.1.9]
Then,
\(\mathsf{v_{eth}}\) of \(\mathrm{In}\) should be less than \(2^{245}\)
\(\mathsf{v_{erc20}}\) of \(\mathrm{In}\) should be less than \(2^{245}\)
\(\mathsf{v_{eth}}\) of \(\mathrm{Out}\) should be less than \(2^{245}\)
\(\mathsf{v_{erc20}}\) of \(\mathrm{Out}\) should be less than \(2^{245}\)
Let \(\mathsf{v^{(\mathrm{In})}_{eth}}\) be the Ether value of the note \(\mathsf{N}\) of \(\mathrm{In}\)
Let \(\mathsf{v^{(\mathrm{Out})}_{eth}}\) be the Ether value of the note \(\mathsf{N}\) of \(\mathrm{Out}\)
Let \(f\) be defined in [3.1.10]
Then,
\[ \Sigma_{i=1}^{x} \mathsf{v^{(\mathrm{In}_i)}_{eth}} = \Sigma_{i=1}^{y} \mathsf{v^{(\mathrm{Out}_i)}_{eth}} + f \]
Let \(\mathsf{v^{(\mathrm{In})}_{erc20}}\) be the ERC20 value of the note \(\mathsf{N}\) of \(\mathrm{In}\)
Let \(\mathsf{v^{(\mathrm{Out})}_{erc20}}\) be the ERC20 value of the note \(\mathsf{N}\) of \(\mathrm{Out}\)
Let \(\mathsf{addr^{(\mathrm{In})}_{token}}\) be the token value of the note \(\mathsf{N}\) of \(\mathrm{In}\).
Let \(\mathsf{addr^{(\mathrm{Out})}_{token}}\) be the token value of the note \(\mathsf{N}\) of \(\mathrm{Out}\).
Let's define a fillter function \(ftr(x,y)\) as \(ftr(x,y) = \left\{\begin{array}{lr} 0 & \text{for } x \neq y\\ 1 & \text{for } x = y\\ \end{array}\right\}\)
Then for each \(\mathsf{addr} \in \{\mathsf{addr^{(\mathrm{In}_1)}_{token}}, ..., \mathsf{addr^{(\mathrm{In}_x)}_{token}}, \mathsf{addr^{(\mathrm{Out}_1)}_{token}}, ..., \mathsf{addr^{(\mathrm{Out}_y)}_{token}}\}\)
It should satisfy
\[
\Sigma_{i=1}^{x} \mathsf{v^{(\mathrm{In}_i)}_{erc20}}\cdot ftr(\mathsf{addr}, \mathsf{addr^{(\mathrm{In}_i)}_{token}}) = \Sigma_{i=1}^{y} \mathsf{v^{(\mathrm{Out}_i)}_{erc20}}\cdot ftr(\mathsf{addr}, \mathsf{addr^{(\mathrm{Out}_i)}_{token}})
\]
Let \(\mathsf{v^{(\mathrm{In})}_{nft}}\) be the NFT id of the note \(\mathsf{N}\) of \(\mathrm{In}\)
Let \(\mathsf{v^{(\mathrm{Out})}_{nft}}\) be the NFT id of the note \(\mathsf{N}\) of \(\mathrm{Out}\)
Let \(\mathsf{addr^{(\mathrm{In})}_{token}}\) be the token value of the note \(\mathsf{N}\) of \(\mathrm{In}\).
Let \(\mathsf{addr^{(\mathrm{Out})}_{token}}\) be the token value of the note \(\mathsf{N}\) of \(\mathrm{Out}\).
Let \(ftr\) be defined in [3.2.4.6.2]
Then for each \(\mathsf{addr}\) and \(\mathsf{nft}\) where
\[
\mathsf{addr} \in \{\mathsf{addr^{(\mathrm{In}_1)}_{token}}, ..., \mathsf{addr^{(\mathrm{In}_x)}_{token}}, \mathsf{addr^{(\mathrm{Out}_1)}_{token}}, ..., \mathsf{addr^{(\mathrm{Out}_y)}_{token}}\} \\
\mathsf{nft} \in \{\mathsf{v^{(\mathrm{In}_1)}_{nft}}, ..., \mathsf{v^{(\mathrm{In}_x)}_{nft}}, \mathsf{v^{(\mathrm{Out}_1)}_{nft}}, ..., \mathsf{v^{(\mathrm{Out}_y)}_{nft}}\}
\]
It should satisfy
\[
\Sigma_{i=1}^{x} ftr(\mathsf{nft}, \mathsf{v^{(\mathrm{In}_i)}_{nft}})\cdot ftr(\mathsf{addr}, \mathsf{addr^{(\mathrm{In}_i)}_{token}}) = \Sigma_{i=1}^{y} ftr(\mathsf{nft}, \mathsf{v^{(\mathrm{Out}_i)}_{nft}})\cdot ftr(\mathsf{addr}, \mathsf{addr^{(\mathrm{Out}_i)}_{token}})
\]
Let \(\mathbb{G_1}\) and \(\mathbb{G_2}\) be defined in [1.15]
Let \(\mathsf{C}_{(x,y)}\) be defined in [3.2.1].
Let \(\mathsf{zPK}_{(x,y)}\) be defined in [3.2.2].
Let \(\mathsf{w}\) be defined in [3.2.4]
Let \(\mathsf{prove_{groth16}}\) be the SNARK proving system defined in https://eprint.iacr.org/2016/260.pdf
Then the prover can generate the SNARK proof \(\pi\) using the proving key \(\mathsf{zPK}_{(x,y)}\):
\[
\pi \leftarrow \mathsf{prove_{groth16}}(\mathsf{w}, \mathsf{zPK}_{(x,y)}) \\
\pi = (\mathbf{A}, \mathbf{B}, \mathbf{C})
\]
where \(\mathbf{A, C} \in \mathbb{G_1}\) and \(\mathbf{B} \in \mathbb{G_2}\)
Let \(\mathsf{C}_{(x,y)}\) be defined in [3.2.1].
Let \(\mathsf{zVK}_{(x,y)}\) be defined in [3.2.3].
Let \(\mathcal{Flow}\) be defined in [3.1.9]
Let \(f, \mathsf{swap}\) be defined in [3.1.10]
Then define the public signal set \(\mathcal{P}\) as
\[ \mathcal{P} = (\mathcal{Flow}, f, \mathsf{swap}) \]
then verifiers can verify the zero-knowledge proof using
\[ \mathsf{verify_{groth16}}(\mathcal{P}, \pi, \mathsf{zVK}_{(x,y)}) \in \{0, 1\} \]
As all information is shielded properly, there should be a memo field to help the recipient decode the receiving note correctly. As it's an optimistic rollup, the size of calldata increases the transaction cost. Therefore, memo filed only supports output notes that have only one value among Ether, ERC20 and NFT.
Note that the memo field is used for the easy communication without constructing any additional p2p networking layer between wallets.
Let \(\mathbf{B}\) be defined in [1.6]
Let \(\mathbf{V}\) be defined in [1.12]
Let \(\mathsf{salt}\) be defined in [2.1]
Let \(\mathrm{Tx}\) be defined in [3.1.10]
Let \(\mathsf{tokenId()}\) be defined in [4.6.1].
Let \(\mathsf{encode()}\) be the point encode function that is defined in [1.13]
The transaction builder pick 1 output \(\mathrm{Out}\) from \(\mathrm{Tx}\) to include in the memo.
Generate a public shared key using ECDH
Prepare the data to encrypt
Run encryption using the shared key with chacha20
\(\mathsf{secret} = \mathsf{(salt, tokenId, v)}\)
\(\mathsf{ciphertext} \leftarrow \mathsf{chacha20(secret, key)}\)
Pack
To receive Zkopru note, recipient should try to decrypt memo field to find own notes.
Let \(\mathcal{Out}\) be defined in [3.1.1]
Let \(\mathcal{Flow}\) be defined in [3.1.9]
Let \(\mathsf{encode()}\) be the point encode function that is defined in [1.13]
Let \(\pi\) be defined in [3.2.5]
Let \(\mathcal{P}\) be defined in [3.2.6]
Let \(\mathsf{memo}\) be defined in [3.3.1]
Then, the shielded transaction \(\mathcal{Tx}\) is
\[
\mathcal{Tx} = (\mathcal{P}, \pi, \mathsf{memo})
\]
where the public signals
\[
\mathcal{P} = (\mathcal{[In_1, ..., In_m], [Out_1, ..., Out_n]}, f, \mathsf{swap})
\]
Zkopru's optimistic rollup manages the singleton storage variable chain
which type is
struct Blockchain {
bytes32 genesis;
bytes32 latest;
// For coordinating
uint256 proposedBlocks;
mapping(address => Proposer) proposers;
mapping(bytes32 => Proposal) proposals;
mapping(bytes32 => bool) finalized; // blockhash => finalized?
mapping(bytes32 => bool) slashed; // blockhash => slashed
// For inclusion reference
mapping(bytes32 => bytes32) parentOf; // childBlockHash => parentBlockHash
mapping(bytes32 => uint256) utxoRootOf; // blockhash => utxoRoot
mapping(uint256 => bool) finalizedUTXORoots; // all finalized utxo roots
// For deposit
MassDeposit stagedDeposits;
uint256 stagedSize;
uint256 massDepositId;
mapping(bytes32 => uint256) committedDeposits;
// For withdrawal
mapping(bytes32 => uint256) withdrawalRootOf; // header => withdrawalRoot
mapping(bytes32 => bool) withdrawn;
mapping(bytes32 => address) newWithdrawalOwner;
// For migrations
mapping(bytes32 => bool) migrationRoots;
mapping(bytes32 => mapping(bytes32 => bool)) transferredMigrations;
// For ERC20 and ERC721
mapping(address => bool) registeredERC20s;
mapping(address => bool) registeredERC721s;
}
contract Zkopru ... {
...
Blockchain chain;
...
}
Let's denote the chain
variable on the address addr
as Zkopru(addr).chain
. In addition we will denote the latest block of a Zkopru network on address addr
like Zkopru(addr).chain.latest
Symbol | Value | Description |
---|---|---|
\(C_{utxo-tree-depth}\) | 48 | UTXO tree depth. It affects the SNARK proving time. |
\(C_{max-utxo}\) | 281474976710656 | We can use this tree for about 45000 years when we have 100 TPS with 2 output notes for 1 transaction. |
\(C_{withdrawal-tree-depth}\) | 48 | UTXO tree depth. |
\(C_{max-withdrawal}\) | 281474976710656 | We can use this tree for about 90000 years when we have 100 TPS with 1 withdrawal note for 1 transaction. |
\(C_{nullifier-tree-depth}\) | 254 | Nullifier tree's depth is 254. |
\(C_{utxo-sub-tree-depth}\) | 5 | A UTXO subtree's depth is 5. |
\(C_{utxo-sub-tree-size}\) | 32 | A UTXO subtree has 32 leaves. |
\(C_{withdrawal-sub-tree-depth}\) | 5 | A Withdrawal subtree's depth is 5. |
\(C_{withdrawal-sub-tree-size}\) | 32 | A withdrawal subtree has 32 leaves. |
\(C_{max-block-size}\) | 200000 | Block should not be too large. Unit in byte. |
\(C_{max-validation-gas}\) | 6000000 | Block proposer should not submit a block which validation process exceeds the given gas limit |
\(C_{challenge-period}\) | 46253 | Challenge period in block number unit. |
\(C_{minimum-stake}\) | 32e18 | Minimum amount of Ether staking to propose a block. |
\(C_{ref-depth}\) | 128 | Recent \(C_{ref-depth}\) UTXO tree roots are available to be used for the UTXO Merkle proof reference. |
Its solidity form looks like:
contract Config {
uint256 public constant UTXO_TREE_DEPTH = 48;
uint256 public constant MAX_UTXO = (1 << UTXO_TREE_DEPTH);
uint256 public constant WITHDRAWAL_TREE_DEPTH = 48;
uint256 public constant MAX_WITHDRAWAL = (1 << WITHDRAWAL_TREE_DEPTH);
uint256 public constant NULLIFIER_TREE_DEPTH = 254;
uint256 public constant UTXO_SUB_TREE_DEPTH = 5; // 32 items at once
uint256 public constant UTXO_SUB_TREE_SIZE = 1 << UTXO_SUB_TREE_DEPTH;
uint256 public constant WITHDRAWAL_SUB_TREE_DEPTH = 5; // 32 items at once
uint256 public constant WITHDRAWAL_SUB_TREE_SIZE =
1 << WITHDRAWAL_SUB_TREE_DEPTH;
uint256 public MAX_BLOCK_SIZE = 200000; // 3.2M gas for calldata
uint256 public MAX_VALIDATION_GAS = 6000000; // 6M gas
// 46523 blocks when the challenge period is 7 days and average block time is 13 sec
uint256 public CHALLENGE_PERIOD = 46523;
uint256 public MINIMUM_STAKE = 32 ether;
uint256 public REF_DEPTH = 128;
}
Let \(\mathsf{Tree_\mathsf{utxo}}\) be defined in [5.3.1].
Let's define deposit \(\mathrm{D} = (\mathrm{U}, f)\) where \(f\) is fee for the block proposer and \(\mathrm{U}\) is the UTXO that'll be included in \(\mathsf{Tree_\mathsf{utxo}}\) later.
Let's define \(keccak256_2\) as
\(keccak256_2(a, b) = keccak256(c)\)
Where \(c\) is a 64 bytes data which is the concatenation of two 32 bytes numbers \(a\) and \(b\) with big-endian.
Zkopru does not store the deposit notes on the contract to minimize the storage gas cost. Instead of storing the deposit notes, it only stores the merged value of all deposits in one storage slot. It could also use a Merkle tree but sequential merging is much gas efficient.
So, let's say we have an array of hashes like \([h_1, h_2, ..., h_n]\).
where
\(\mathsf{merge}([]) = 0\)
\(\mathsf{merge}([h1]) = keccak256_2(0, h_1)\)
\(\mathsf{merge}([h_1, h_2]) = keccak256_2(h_1, h_2)\)
\(\mathsf{merge}([h_1, h_2, h_3]) = keccak256_2(\mathsf{merge}([h_1, h_2]), h_3)\)
\(\mathsf{merge}([h_1, h_2, ..., h_n])
= keccak256_2(\mathsf{merge}([h_1, h_2, ..., h_{n-1}]), h_{n})\)
So we can express this \(\mathsf{merge}\) function as an recursive form like,
\[ \mathsf{merge}([h_1, h_2, ..., h_n]) = \mathsf{merge}([\mathsf{merge}([h_1, h_2, ..., h_{n-1}]), h_{n}]) \]
Smart contract has one staged deposits that can be a Mass Deposit. Every deposit()
function will update the staged deposits and will merge the deposit data into it.
Here is how a deposit \(\mathrm{D}\) by deposit()
updates the staged deposits \(\mathrm{MD}_{stage}\).
Let \([\mathrm{D_1},\mathrm{D_2}, ...,\mathrm{D_n}]\) are appended to the staged deposits.
Let \(\mathsf{merge()}\) be defined in [4.2.2.1].
Let \(f_i\) is the deposit fee for \(\mathrm{D}_i\).
Then,
\[ \mathrm{MD}_{stage} = (\mathsf{merged}, f_{MD}) \]
where
\[ \mathsf{merged} = \mathsf{merge}([\mathsf{hash}(\mathrm{D_1}), \mathsf{hash}(\mathrm{D_2}), ..., \mathsf{hash}(\mathrm{D_n})]) \\ f_{MD} = \Sigma_{i=0}^{n} f_i \]
Finally, the deposit()
transaction transfers assets and stores the updated \(\mathrm{MD}_{stage}\) to Zkopru.chain.stagedDeposits
.
Anyone can commit the staged deposits and create a Mass Deposit from it by calling commitMassDeposit()
. Then, the layer 1 contract records it as committed and starts a new empty staged deposit object.
Let \([\mathrm{D_1},\mathrm{D_2}, ...,\mathrm{D_n}]\) construct a mass deposit \(\mathrm{MD}\).
Then, the mass deposit \(\mathrm{MD}\) is expressed with
\[
\mathrm{MD} = (\mathsf{merged}_\mathrm{MD}, f_\mathrm{MD})
\]
where
\[ \mathsf{merged}_\mathrm{MD} = \mathsf{merge}([\mathsf{hash}(\mathrm{D_1}), \mathsf{hash}(\mathrm{D_2}), ..., \mathsf{hash}(\mathrm{D_n})]) \]
Finally, commitMassDeposit()
stores \(\mathrm{MD}\) to the Zkopru.chain.committedDeposits
setting the key with its hash value.
The hash of a mass deposit is
\[
\mathsf{hash}(\mathrm{MD}) = keccak256(\mathsf{encodePacked}(\mathsf{merged}_\mathrm{MD}, f_\mathrm{MD})).
\]
To withdraw \(\mathrm{W}\) out of the Zkopru network, there should be an existence proof and the ownership proof.
Let \(\mathcal{W}\) be defined in [3.1.3].
Let \(\mathsf{Tree_\mathsf{withdrawal}}\) be defined in [5.3.2].
\(\mathsf{pos}(\mathcal{W})\) is the leaf index of \(\mathcal{W}\) in the Withdrawal Merkle tree \(\mathsf{Tree}_\mathsf{withdrawal}\).
Let \(\mathsf{hash}(\mathcal{W})\) be defined in [3.1.3].
Let \(\mathsf{pos}(\mathcal{W})\) be defined in [4.3.1.1].
Let \(\mathsf{Tree_\mathsf{withdrawal}}\) be defined in [5.3.2].
Let \(\mathsf{root_{withdrawal}}^{(n)}\) be defined in [6.1.2].
Let the root of \(\mathsf{Tree}_\mathsf{withdrawal}\) at the \(n\)-th layer 2 block be denoted \(\mathsf{root_{withdrawal}}^{(n)}\).
\(\mathsf{Sib}_\mathsf{withdrawal}^{(n)}(\mathcal{W})\) is the set of all sibling node data of Withdrawal \(\mathcal{W}\) at the \(n\)-th layer 2 block.
Then the block \(\mathsf{Block}^{(n)}\) and its corresponding \(\mathsf{root_{withdrawal}}^{(n)}\) should be recorded as finalized on the smart contract by the optimistic rollup consensus.
Then, withdraw()
transaction should include a Merkle Proof that proves
\[
\mathsf{Merkle}^{(n)}_\mathsf{withdrawal}(\mathcal{W}) = (\mathsf{hash}(\mathcal{W}), \mathsf{pos}(\mathcal{W}), \mathsf{root_{withdrawal}}^{(n)}, \mathsf{Sib}_\mathcal{W}^{(n)}(\mathcal{W}))
\]
Let \(\mathsf{N}, \mathcal{N}\) be defined in [3.1.1]
Let \(\mathcal{W}\) be defined in [3.1.3]
Layer 1 transaction withdraw()
should include \(\mathsf{hash(N)}\) and \(\mathcal{N}\) that satisfy \(\mathsf{hash(\mathcal{W})}\).
Then, the submitted \(\mathsf{fee_{caller}}\) amount of Ether goes to the withdraw()
transaction executor.
As the user should wait the finalization, \(\mathcal{W}\) owner can request an instant withdrawal to prepayers who are willing to pay the fund in advance to earn fee.
For the instant withdrawal with pay in advance feature,
struct PrepayRequest {
address prepayer;
bytes32 withdrawalHash;
uint256 prepayFeeInEth;
uint256 prepayFeeInToken;
uint256 expiration;
}
payInAdvance()
function with the received ECDSA signature and correct amount of assets to pay in advance for the original owner.withdrawalHash
.Let \(\mathcal{M}\) be defined in [3.1.4]
Let \(M\) be the set of all migration type transaction outputs of a block.
Let \(A_\mathsf{dest}\) be the set of all destination addresses of the migration type transaction outputs of the block.
Let \(A_\mathsf{token}\) be the set of all token addresses that exist in the migration outputs.
Let \(\mathsf{merge()}\) be defined in [4.2.2.1]
Then, for each \(\mathsf{dest} \in A_\mathsf{dest}\), \(\mathsf{token} \in A_\mathsf{token}\) we can construct a set of migrations with
\(M_{\mathsf{dest}} =\{ \mathcal{M} | \mathcal{M}.\mathsf{to} = \mathsf{dest} \land \mathcal{M}.\mathsf{token} = \mathsf{token} \} = \{\mathcal{M_1, M_2, ..., M_k}\}\)
Let \(ftr\) be defined in [3.2.4.6.2]
Then, for each \((\mathsf{dest}, \mathsf{token})\) pair where \(\mathsf{dest} \in A_\mathsf{dest}\) and \(\mathsf{token} \in A_\mathsf{token}\) has its corresponding mass migration \(\mathrm{MM}\) defined as
\(\mathrm{MM} = (\mathsf{dest}, \mathsf{asset_{migration}}, \mathrm{MD}_\mathsf{migration})\)
where
Let a block contain mass migrations \([\mathrm{MM}_1, ..., \mathrm{MM}_n]\)
and \((\mathsf{dest}, \mathsf{token})_i = (\mathrm{MM}_i.\mathsf{dest}, \mathrm{MM}_i.\mathsf{token})\)
Then \((\mathsf{dest}, \mathsf{token})_i \neq (\mathsf{dest}, \mathsf{token})_j\) when \(i \neq j\).
Zkopru can have various types of proposer selection logic. And it can be simply fetched by calling isProposable(address)
function which type is
function isProposable(address proposer) pure returns (bool);
This function asks the proposability of the given address to the ConsensusProvider
which default is the "BurnAuction" for now.
To propose a block, the coordinator should have staked more than 32 ETH in the contract. Coordinator can stake ETH by calling register()
.
Let \(C_{max-block-size}\) be defined in [4.1.2].
Once the coordinator has proposer amount of stakes, proposer can submit a serialized form of \(\mathtt{Block}\) as defined in [6.7.1] by calling propose(bytes calldata)
function.
To call the function propose()
, it requires
msg.sender
should have staked more than 32 ETH.msg.sender
.Once the function propose()
is called,
Zkopru.chain.proposers.exitAllowance
to its challenge due block number that is block.number
+\(C_{challenge-preiod}\).Coordinators can withdraw their staked ETH whenever Zkopru.chain.proposers.exitAllowance
is behind the block number.
Let \(\mathtt{Finalization}^{(n)}\) be defined in [6.7.2].
Anyone can call the finalize(bytes calldata)
function by submitting the finalization data \(\mathtt{Finalization}^{(n)}\).
Using the \(\mathsf{checksum}\) of the proposal parsed from \(\mathtt{Finalization}^{(n)}\) retrieve the proposal object proposal
from Zkopru.chain.proposals[checksum]
.
To finalize a block, it requires
proposal
.proposal
should not be slashed.proposal
should not be finalized.Zkopru.chain.latest
should equal to the \(\mathsf{parent}\) of \(\mathsf{Header}^{(n)}\).proposal.challengeDue
should be behind block.number
.Zkopru.chain.committedDeposits
.Zkopru.chain.migrationRoots
.Once the function finalize()
is called,
Zkopru.chain.committedDeposits
.Zkopru.chain.migrationRoots
. It allows migrateFrom()
function call in [4.5.6].Zkopru.chain.finalized
Zkopru.chain.finalizedUTXORoots
latest
.proposal
.migrateFrom
Let \(\mathrm{MM}\) be defined in [4.4.1]
Let \(\mathsf{migrationRoot}^{(n)}\) be defined in [4.5.5] and \(\mathrm{MM}^{(n)}_i\) is one of migrations in that Merkle tree.
After \(\mathsf{migrationRoot}^{(n)}\) is finalized on the contract, the destination network of \(\mathrm{MM}^{(n)}_i\) can call migrateFrom
function to migrate \(\mathsf{asset}_\mathsf{migration}\) with its MerkleProof.
Then, it records \((\mathsf{migrationRoot}^{(n)}, \mathsf{hash}(\mathrm{MM}^{(n)}_i))\) as transferred in Zkopru(source).chain.transferredMigratios
. Simultaneously, it transfers the given ETH and tokens to the \(\mathsf{dest}\) network adding \(\mathrm{MD}_\mathsf{migration}\) to Zkopru(dest).chain.committedDeposits
.
Zkopru transaction's encrypted memo field uses token id instead of its full address to reduce the data size.
If the token address is \(a\), \(\mathsf{tokenId(a)} = a \ mod \ 256\).
Anyone can register any kind of ERC20 Token that follows the standard interface. Once the testing transaction is succeed, Zkopru contract will register the token address into the Zkopru.chain.registeredERC20s
.
Anyone also can register any kind of ERC721 Token that implements ERC165 standard. If the token contract's ERC165 interface returns true against the query for ERC721 support, Zkopru contract will register the token address into the Zkopru.chain.registeredERC721s
.
Let's assume that a proposer submitted \(\mathsf{Block}^{(n)}\).
Let \(\mathrm{MD}\) is one of the submitted mass deposit in \(\mathsf{Block}^{(n)}\).
Then, if any of the \(\mathsf{hash}(\mathrm{MD})\) does not exists in Zkopru.chain.committedDeposits
, the validation contract returns slashable = true
with code D1.
Let \(\mathsf{MerkleRoot}\) be defined in [7.1]
Let \(\mathtt{MDs}\) is the array of submitted mass deposits in \(\mathsf{Block}^{(n)}\).
Let \(\mathsf{depositRoot}^{(n)}\) be defined in [6.2.1]
When \(\mathsf{MerkleRoot}_{keccak256}(\mathtt{MDs})\) does not equal to \(\mathsf{depositRoot}^{(n)}\), the validation contract returns slashable = true
with code H1.
Let \(\mathsf{MerkleRoot}\) be defined in [7.1]
Let \(\mathtt{TXs}\) is the array of submitted transactions in \(\mathsf{Block}^{(n)}\).
Let \(\mathsf{txRoot}^{(n)}\) be defined in [6.2.1]
When \(\mathsf{MerkleRoot}_{keccak256}(\mathtt{TXs})\) does not equal to \(\mathsf{txRoot}^{(n)}\), the validation contract returns slashable = true
with code H2.
Let \(\mathsf{MerkleRoot}\) be defined in [7.1]
Let \(\mathtt{MMs}\) is the array of submitted mass migrations in \(\mathsf{Block}^{(n)}\).
Let \(\mathsf{migrationRoot}^{(n)}\) be defined in [6.2.1]
When \(\mathsf{MerkleRoot}_{keccak256}(\mathtt{MMs})\) does not equal to \(\mathsf{migrationRoot}^{(n)}\), the validation contract returns slashable = true
with code H3.
The total fee for block proposer should equal to
\[ \mathsf{fee}^{(n)} = \Sigma_{i=0}^{n_{tx}} \mathcal{Tx}_i.\mathcal{P}.f + \Sigma_{i=0}^{n_{md}} \mathrm{MD}_i.f_{MD} \]
Or validation contract returns slashable = true
with code H4.
Then, \(\mathsf{parent}^{(n)}\) should not be a slashed block. So if Zkopru.chain.slashed[parent]
exists, the validation contract returns slashable = true
with code H5.
Let \(\mathrm{MM}\) is one of the submitted mass migration in \(\mathsf{Block}^{(n)}\).
If it does not satisfy the condition [4.4.2] that every migration should have different destination, the validation contract returns slashable = true
with code M1.
Let \(\mathrm{MM}\) is one of the submitted mass migration in \(\mathsf{Block}^{(n)}\).
If it does not satisfy the following condition,
\[
\mathrm{MM}.\mathsf{asset_{migration}}.\mathsf{eth} = \Sigma \mathcal{M}.\mathsf{v_{eth}}
\]
If the sum of total ETH does not equal to the \(\mathsf{eth}\) defined in the migrating asset, and the validation contract returns slashable = true
with code M2.
Let \(\mathrm{MM}\) is one of the submitted mass migration in \(\mathsf{Block}^{(n)}\).
If it does not satisfy the following condition,
\[
\mathrm{MM}.\mathsf{asset_{migration}}.\mathsf{amount} = \Sigma \mathcal{M}.\mathsf{v_{erc20}}
\]
If the sum of total token amount does not equal to the \(\mathsf{amount}\) defined in the migrating asset, and the validation contract returns slashable = true
with code M3.
Let \(\mathrm{MM}\) is one of the submitted mass migration in \(\mathsf{Block}^{(n)}\).
If the mass deposit fot the destination does not equal to the on-chain computed mass deposit
\[
\mathrm{MD}_\mathsf{migration} = (\mathsf{merge(\mathcal{[M_1, ..., M_k]})}, \Sigma_{i=0}^{k}\mathcal{M}.\mathsf{fee_{migration}})
\]
the validation contract returns slashable = true
with code M4.
Let \(\mathrm{MM}\) is one of the submitted mass migration in \(\mathsf{Block}^{(n)}\).
If it does not satisfy the \(\mathsf{fee}\) condition in [4.4.1]
\[
\mathrm{MD} = (\mathsf{merge(\mathcal{[M_1, ..., M_k]})}, \Sigma_{i=0}^{k}\mathcal{M}.\mathsf{fee_{migration}})
\]
the validation contract returns slashable = true
with code M5.
Let \(\mathrm{MM}\) is one of the submitted mass migration in \(\mathsf{Block}^{(n)}\).
If \(\mathcal{M}.\mathsf{token}\)a does not exists in the contract storage Zkopru.chain.registeredERC20s
it returns slashable = true
with code M6.
For every \(\mathcal{Tx}\), a mass migration should exists in the \(\mathrm{MM}[]\) list that correspondes its destination and carrying token address. If the block body misses any destination or token, the validation contract returns slashable = true
with code M7.
Let \(\mathtt{TXs}^{(n)}\) and \(\mathtt{Tx}^{(n)}_i\) be defined in [6.6.5]
Let \(\mathcal{Tx}^{(n)}_i\) be the deserialized form of \(\mathtt{TX}^{(n)}_i\).
Let \(\mathsf{Tree_{nullifier}}^{(n)}\) be defined in [5.3.3].
Let \(\mathsf{nullifiers}^{(n)} = \{\mathcal{Tx_i^{(n)}.P.In_j.\mathsf{nullifier}} | 1 \leq i \leq n_{tx}, 1 \leq j \leq 4 \}\)
Then, for every \(\mathsf{nullifier} \in \mathsf{nullifiers}^{(n)}\), should not exist in the \(\mathsf{Tree}_{nullifier}\).
And appending all \(\mathsf{nullifier}\) to \(\mathsf{Tree_{nullifier}}^{(n-1)}\), its updated root should equal to \(\mathsf{root}^{(n)}_\mathsf{nullifier}\).
Otherwise, the validation contract returns slashable = true
with code N1.
Let \(\mathtt{TXs}^{(n)}\) and \(\mathtt{Tx}^{(n)}_i\) be defined in [6.6.5]
Let \(\mathtt{MDs}^{(n)}\) and \(\mathrm{MD}^{(n)}_i\) be defined in [6.6.5]
Let \(\mathsf{Tree_{utxo}}^{(n)}\) be defined in [5.3.3].
Let \(C_{utxo-sub-tree-size}\) be defined in [4.1.2].
Let \(\mathrm{MD}^{(n)}_i = [\mathrm{D}_{i_1}, \mathrm{D}_{i_2}, ..., \mathrm{D}_{i_{n_i}}]\).
Then the list of deposit notes becomes \(\mathsf{utxos}_{deposits}^{(n)} = [\underbrace{\mathrm{D}_{1_1}, \mathrm{D}_{1_2}, ..., \mathrm{D}_{1_{n_1}}}_{\mathrm{MD}_1}, \underbrace{\mathrm{D}_{2_1}, \mathrm{D}_{2_2}, ..., \mathrm{D}_{2_{n_2}}}_{\mathrm{MD}_2}, ..., \underbrace{\mathrm{D}_{k_1}, \mathrm{D}_{k_2}, ..., \mathrm{D}_{k_{n_k}}}_{\mathrm{MD}_{n_{md}=k}}]\)
Let \(\mathrm{U}_{i_j} = \mathcal{Tx_i^{(n)}.P.Out_j}\).
Then the list of deposit notes becomes \(\mathsf{utxos}_{txs}^{(n)} = [\underbrace{\mathrm{O}_{1_1}, \mathrm{O}_{1_2}, ..., \mathrm{O}_{1_{n_1}}}_{\mathcal{Tx}_1}, \underbrace{\mathrm{O}_{2_1}, \mathrm{O}_{2_2}, ..., \mathrm{O}_{2_{n_2}}}_{\mathcal{Tx}_2}, ..., \underbrace{\mathrm{O}_{l_1}, \mathrm{O}_{l_2}, ..., \mathrm{O}_{l_{n_l}}}_{\mathcal{Tx}_{n_{tx} = l}}]\)
Then the total list of all utxos becomes
\[ \mathsf{utxos}^{(n)} = [\underbrace{\mathrm{D}_{1_1}, \mathrm{D}_{1_2}, ..., \mathrm{D}_{k_{n_k}}}_{\mathsf{utxos}^{(n)}_{deposits}}, \underbrace{\mathrm{O}_{1_1}, \mathrm{O}_{1_2}, ..., \mathrm{O}_{l_{n_l}}}_{\mathsf{utxos}^{(n)}_{txs}}, \underbrace{0, 0, ..., 0}_\text{padded zeroes}] \]
Here, the padded zeroes are added to make the length of \(\mathsf{utxos}^{(n)}\) be the multiple of \(C_{utxo-sub-tree-size}\).
Therefore
\[
len(\mathsf{utxos}^{(n)}) \ mod \ C_{utxo-sub-tree-size} = 0
\]
Let \(\mathsf{utxos}^{(n)}\) be defined in [4.7.5].
Let \(\mathsf{index_{utxos}}^{(n)}\) be defined in [6.2.1].
Then,
\[
len(\mathsf{utxos}^{(n)}) + \mathsf{index_{utxos}}^{(n-1)} = \mathsf{index_{utxos}}^{(n)}
\]
Otherwise, the validation contract returns slashable = true
with code U1.
Let \(\mathsf{utxos}^{(n)}\) be defined in [4.7.5].
Let \(\mathsf{index_{utxo}}^{(n)}\) be defined in [6.2.1].
Let \(C_{max-utxo}\) be defined in [4.1.2].
Then,
\[
len(\mathsf{utxos}^{(n)}) + \mathsf{index_{utxos}}^{(n-1)} \leq C_{max-utxo}
\]
Otherwise, the validation contract returns slashable = true
with code U2.
Let \(\mathsf{Tree_{utxo}}^{(n)}\) be defined in [5.3.1].
Let \(\mathsf{utxos}^{(n)}\) be defined in [4.7.5].
Let \(\mathsf{root_{utxo}}^{(n)}\) be defined in [6.2.1].
Let \(C_{max-utxo}\) be defined in [4.1.2].
Then, appending the hash of each items in \(\mathsf{utxos}^{(n)}\) to \(\mathsf{Tree_{utxo}}^{(n-1)}\) should have an updated root that equals to \(\mathsf{root_{utxo}}^{(n)}\).
Otherwise, the validation contract returns slashable = true
with code U3.
Let \(\mathtt{TXs}^{(n)}\) and \(\mathtt{Tx}^{(n)}_i\) be defined in [6.6.5]
Let \(\mathsf{Tree_{withdrawal}}^{(n)}\) be defined in [5.3.2].
Let \(C_{withdrawal-sub-tree-size}\) be defined in [4.1.2].
Let \(\mathcal{W}_{i_j} = \mathcal{Tx_i^{(n)}.P.Out_j}\) only if \(\mathcal{Tx_i^{(n)}.P.Out_j}.t = 1\).
Then the list of deposit notes becomes \(\mathsf{withdrawals}^{(n)} = [\underbrace{\mathcal{W}_{1_1}, \mathcal{W}_{1_2}, ..., \mathcal{W}_{1_{n_1}}}_{\mathcal{Tx}_1}, \underbrace{\mathcal{W}_{2_1}, \mathcal{W}_{2_2}, ..., \mathcal{W}_{2_{n_2}}}_{\mathcal{Tx}_2}, ..., \underbrace{\mathcal{W}_{k_1}, \mathcal{W}_{k_2}, ..., \mathcal{W}_{k_{n_k}}}_{\mathcal{Tx}_{n_{tx} = k}}, \underbrace{0, 0, ..., 0}_\text{padded zeroes}]\)
Here, the padded zeroes are added to make the length of \(\mathsf{withdrawals}^{(n)}\) be the multiple of \(C_{withdrawal-sub-tree-size}\).
Therefore
\[
len(\mathsf{withdrawals}^{(n)}) \ mod \ C_{withdrawal-sub-tree-size} = 0
\]
Let \(\mathsf{withdrawals}^{(n)}\) be defined in [4.7.6].
Let \(\mathsf{index_{withdrawals}}^{(n)}\) be defined in [6.2.1].
Then,
\[
len(\mathsf{withdrawals}^{(n)}) + \mathsf{index_{withdrawals}}^{(n-1)} = \mathsf{index_{withdrawals}}^{(n)}
\]
Otherwise, the validation contract returns slashable = true
with code W1.
Let \(\mathsf{withdrawals}^{(n)}\) be defined in [4.7.6].
Let \(\mathsf{index_{withdrawal}}^{(n)}\) be defined in [6.2.1].
Let \(C_{max-withdrawal}\) be defined in [4.1.2].
Then,
\[
len(\mathsf{withdrawals}^{(n)}) + \mathsf{index_{withdrawals}}^{(n-1)} \leq C_{max-withdrawal}
\]
Otherwise, the validation contract returns slashable = true
with code W2.
Let \(\mathsf{Tree_{withdrawal}}^{(n)}\) be defined in [5.3.2].
Let \(\mathsf{withdrawals}^{(n)}\) be defined in [4.7.6].
Let \(\mathsf{root_{withdrawal}}^{(n)}\) be defined in [6.2.1].
Let \(C_{max-withdrawal}\) be defined in [4.1.2].
Let \(\mathsf{hash}(\mathcal{W})\) be defined in [3.1.3].
Then, appending the withdrawal hash of each items in \(\mathsf{withdrawals}^{(n)}\) to \(\mathsf{Tree_{withdrawal}}^{(n-1)}\) should have an updated root that equals to \(\mathsf{root_{withdrawal}}^{(n)}\).
Otherwise, the validation contract returns slashable = true
with code W3.
Let \(C_{ref-depth}\) be defined in [4.1.2].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, In_2, ...], [Out_1, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
Then for every \(\mathcal{In} = (\mathsf{nullifier(\mathrm{U})}, \mathsf{root_{utxo}}^{(k)})\), it should satisfy
\[
n - C_{ref-depth} \leq k
\]
or
\(\mathsf{root_{utxo}}^{(k)}\) should be stored in the Zkopru.chain.finalizedUTXORoots
.
Otherwise, the validation contract returns slashable = true
with code T1.
Let \(\mathcal{Out} = (\mathsf{hash(N)}, t, \mathcal{N})\) as defined in [3.1.1].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
Then for every \(\mathcal{Out}\),
\[ 0 \leq t \leq 2 \]
Otherwise, the validation contract returns slashable = true
with code T2.
Let \(\mathcal{Out} = (\mathsf{hash(N)}, t, \mathcal{N})\) as defined in [3.1.1].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
Then, for every \(\mathcal{Out}\),
\(\mathcal{N}\) should be \((0, 0, 0, 0, 0, 0)\) if and only if \(t = 0\).
Otherwise, the validation contract returns slashable = true
with code T3.
Let \(\mathcal{Out} = (\mathsf{hash(N)}, t, \mathcal{N})\) as defined in [3.1.1].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
For every \(\mathcal{Out}\),
\(\mathcal{N}.\mathsf{addr_{token}}\) should be registered on Zkopru.chain.registeredERC20s
or Zkopru.chain.registeredERC721s
.
Otherwise, the validation contract returns slashable = true
with code T4.
Let \(\mathcal{Out} = (\mathsf{hash(N)}, t, \mathcal{N})\) as defined in [3.1.1].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
For every \(\mathcal{Out}\),
\(\mathcal{N}.\mathsf{v'_{nft}}\) should be zero if \(\mathcal{N}.\mathsf{addr_{token}}\) is not registered as an ERC721 on Zkopru.chain.registeredERC721s
.
Otherwise, the validation contract returns slashable = true
with code T5.
Let \(\mathcal{Out} = (\mathsf{hash(N)}, t, \mathcal{N})\) as defined in [3.1.1].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
For every \(\mathcal{Out}\),
\(\mathsf{v'_{erc20}}\) should be zero if \(\mathcal{N}.\mathsf{addr_{token}}\) is not registered as an ERC20 on Zkopru.chain.registeredERC20s
.
Otherwise, the validation contract returns slashable = true
with code T6.
Let \(\mathcal{Out} = (\mathsf{hash(N)}, t, \mathcal{N})\) as defined in [3.1.1].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
For every \(\mathcal{Out}\),
\(\mathcal{N}.\mathsf{v'_{nft}}\) should not be zero if \(\mathcal{N}.\mathsf{addr_{token}}\) is registered as an ERC721 on Zkopru.chain.registeredERC721s
.
Otherwise, the validation contract returns slashable = true
with code T7.
This is because the SNARK circuit is designed not to support NFT id 0 by its technical limitation.
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}_i), \pi_i, \mathsf{memo}_i)\) as defined in [3.4.1].
If \(\mathsf{swap}_i\) is not zero, there should exist another \(\mathcal{Tx}^{(n)}_j\) that has \(\mathsf{swap}_i\) for one of its outputs while \(\mathsf{swap}_j\) is one of \(\mathcal{Tx}^{(n)}_i\)'s output.
If there does not exist correct pair, the validation contract returns slashable = true
with code T8.
Let \(\mathcal{In}\) be defined in [3.1.7].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
Then for every \(\mathcal{In} = (\mathsf{nullifier(\mathrm{U})}, \mathsf{root_{utxo}}^{(k)})\), appending \(\mathsf{nullifier(\mathrm{U})}\) to \(\mathsf{Tree_{nullifier}}^{(n-1)}\) should not update the tree.
Otherwise, it is considered as a used one and the validation contract returns slashable = true
with code T9.
Let \(\mathcal{In}\) be defined in [3.1.7].
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ...], [Out_1, Out_2, ...]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
Then for every \(\mathcal{In} = (\mathsf{nullifier(\mathrm{U})}, \mathsf{root_{utxo}}^{(k)})\), \(\mathsf{nullifier(\mathrm{U})}\) should be unique within the whole nullifiers of other transactions in the same block.
Otherwise, it is considered as a used one and the validation contract returns slashable = true
with code T10.
Let \(\mathcal{Tx}^{(n)}_i = ((\mathcal{[In_1, ..., In_x], [Out_1, ..., Out_y]}, f, \mathsf{swap}), \pi, \mathsf{memo})\) as defined in [3.4.1].
Then, verifyig key for circuit \(\mathsf{C}_{(x,y)}\) should exists on Zkopru.vks
.
Otherwise, the validation contract returns slashable = true
with code S1.
Let \(\mathcal{Tx}^{(n)}_i = (\mathcal{P}, \pi, \mathsf{memo})\) as defined in [3.4.1].
Let \(\mathsf{zVK}_{(x,y)}\) be the verifying key for circuit \(\mathsf{C}_{(x,y)}\).
Then,
\[
\mathsf{verify_{groth16}}(\mathcal{P}, \pi, \mathsf{vPK}_{(x,y)}) = 1
\]
Otherwise, the validation contract returns slashable = true
with code S2.
Let \(p\) be defined in [1.1].
Let \(\mathcal{Tx}^{(n)}_i = (\mathcal{P}, \pi, \mathsf{memo})\) as defined in [3.4.1].
Then, every value of \(\mathcal{P}\) should be less than \(p\).
Otherwise, the validation contract returns slashable = true
with code S3.
Sparse Merkle Tree is a fixed depth Merkle tree that all leaves have a defined initial value. It is defined with \(\mathsf{SMT = (depth, hash, null)}\).
Let \(\mathsf{SMT}\) be defined in [5.1.1].
Then, the tree has \(\mathsf{depth} + 1\) number of layers. For example when the \(\mathsf{depth} = 3\),
Depth 0 (Level 3): d
Depth 1 (Level 2): c-------^-------c
Depth 2 (Level 1): b---^---b b---^---b
Depth 3 (Level 0): a-^-a a-^-a a-^-a a-^-a
And it can include \(2^\mathsf{depth}\) of items.
Let \(\mathsf{SMT}\) be defined in [5.1.1].
Then, it has \(2^{\mathsf{depth} + 1} - 1\) tree nodes and each node has its own index and value.
\[ \mathsf{node = (index, value)} \]
Index starts from the root node with value 1. After then, every left child node's index is the double of its parent's index and the right child node's index is plus one of its sibling left node.
If we express the index in binary format the index map looks like below when the depth is 3:
Depth 0 (Level 3): (1)
Depth 1 (Level 2): (10)-------------^-------------(11)
Depth 2 (Level 1): (100)-----^-----(101) (110)-----^-----(111)
Depth 3 (Level 0): (1000)-^-(1001) (1010)-^-(1011) (1100)-^-(1101) (1110)-^-(1111)
Merkle tree has three types of node.
Let \(\mathsf{depth, null}\) be defined in [5.1.1].
Every leaf node has no child and its initial value is \(\mathsf{null}\).
Then index of leaf node should be greater or equal than \(2^{\mathsf{depth}}\) and less than \(2^{\mathsf{depth} + 1}\).
As \(\mathsf{SMT}\) usually stores items into the leaf nodes, leaf nodes also have \(\mathsf{pos}\) value that is
\[
\mathsf{pos} = \mathsf{node.index} - 2^\mathsf{depth}
\]
\(\mathsf{pos}\) starts from 0 and less than \(2^\mathsf{depth}\)
Let \(\mathsf{hash}\) be defined in [5.1.1].
Every node except leaf node is kind of the branch node, and their value is decided by the values of their children nodes.
Let \(\mathsf{parent}\) be a branch node which has \(\mathsf{left, right}\) for its children nodes.
Then,
\[ \mathsf{parent.value} = \mathsf{hash(left.value, right.value)} \]
Then index of leaf node should be greater or equal than 1 and less than \(2^{\mathsf{depth}}\)
Root node is also a branch node which index is 1. The value of the root node is a compressed state of the tree.
Let \(\mathsf{SMT, hash, depth}\) be defined in [5.1.1].
Let \(\mathsf{node}\) be defined in [5.1.3].
Let \(\mathsf{pos}\) be defined in [5.1.4.1].
Let \(\mathsf{root}\) is the value of the root node of the \(\mathsf{SMT}\).
Let \(\mathsf{item}\) be defined as
\[
\mathsf{item} = (\mathsf{pos}, \mathsf{value})
\]
Then, we can compute the root value using the \(\mathsf{siblings}\) and \(\mathsf{item}\)
\[ \mathsf{root} = \mathsf{SMT.computeRoot(item, siblings)} \]
where \(\mathsf{siblings}\) are the value of all sibling nodes of its Merkle path.
Using \(\mathsf{item}\) and \(\mathsf{siblings}\) we can prove an inclusion of the \(\mathsf{item}\) in the \(\mathsf{SMT}\). And also as \(\mathsf{SMT}\) is a Sparse Merkle tree that has initial \(\mathsf{null}\) value, we can also prove the non-inclusion proof using \(\mathsf{null}\) and \(\mathsf{pos}\).
Here's the reference solidity code of the Merkle Root computation.
uint256 immutable public DEPTH;
function computeRoot(
function(bytes32, bytes32) pure returns(bytes32) hash,
uint256 pos,
bytes32 item,
bytes32[] sibligns
)
public
pure
returns (bytes32)
{
require(siblings.length == DEPTH);
uint256 path = pos;
uint256 node = item;
for (uint256 i = 0; i < siblings.length; i++) {
if (path % 2 == 0) {
// right sibling
node = hash(node, siblings[i]);
} else {
// left sibling
node = hash(siblings[i], node);
}
path >>= 1;
}
return node;
}
Let \(\mathsf{computeRoot}\) be defined in [5.1.5].
Let \(\mathsf{index}\) be the \(\mathsf{pos}\) of the leaf node to update. Append-only merkle tree is a sparse merkle tree that updates leaves from left to right by incrementing \(\mathsf{index}\).
Then we can define the \(\mathsf{append}\) function for \(\mathsf{SMT}\) as
\[ \mathsf{(root_{next}, index_{next}, siblings_{next}) = SMT.append(root_{prev}, index_{prev}, siblings_{prev}, item)} \]
where
\(\mathsf{index_{next}} = \mathsf{index_{prev}} + 1\)
\(\mathsf{root_{prev}} == \mathsf{SMT.computeRoot(index_{prev}, 0, siblings_{prev})}\)
\(\mathsf{root_{next}} == \mathsf{SMT.computeRoot(index_{next}, 0, siblings_{next})}\)
\(\mathsf{root_{next}} == \mathsf{SMT.computeRoot(index_{prev}, item, siblings_{prev})}\)
Let \(\mathsf{append}\) be defined in [5.2.1]
Then, we can define \(\mathsf{batchAppend}\) as
\[ \mathsf{(root_{next}, index_{next}, siblings_{next}) = SMT.batchAppend(root_{prev}, index_{prev}, siblings_{prev}, items)} \]
where
\(\mathsf{(root_{i+1}, index_{i+1}, siblings_{i+1}) = SMT.append(root_{i}, index_{i}, siblings_{i}, item_i)}\)
\(\mathsf{root}_1 = \mathsf{root_{prev}}\)
\(\mathsf{root}_{n+1} = \mathsf{root_{next}}\)
\(\mathsf{[item_1, item_2, ..., item_n] = items}\)
To update the Merkle tree we can insert a small sub-tree instead of updating each leaf. For example, let's assume we're tyring to add 256 items to a tree which depth is 32. Then, we can update the tree with only \(32\) hash computations while we have to compute \(32 \times 256\) hashes when we try to update the tree item by item.
First, let's divide a Sparse Merkle tree with sub-trees and its parent tree. For example,
y
y y
parent tree y y y y
--------------------------------------------------------
sub trees x x x x x x x x
x x x x x x x x x x x x x x x x
----- ----- ----- ----- ----- ----- ----- -----
sub1 sub2 sub3 sub4 sub5 sub6 sub7 sub8
Then we can define the sub-tree and parent tree as Sparse Merkle trees like
\(\mathsf{SubTree} = (d_{sub}, \mathsf{hash}, \mathsf{null})\)
\(\mathsf{ParentTree} = (\mathsf{depth} - d_{sub}, \mathsf{hash}, \mathsf{SubTree.initialRoot})\)
And, divide the items into a fixed size chunks as
\(\mathsf{chunks} = [\mathsf{chunk_1, ..., chunk_k}] = [[sub^{(1)}_1, ..., sub^{(1)}_{n_1}], [sub^{(2)}_1, ..., sub^{(2)}_{n_2}], ..., [sub^{(k)}_1, ..., sub^{(k)}_{n_k}]]\)
where
\(\mathsf{items} = [\mathsf{item_1, ..., item_n}] = [sub^{(1)}_1, ..., sub^{(1)}_{n_1}, sub^{(2)}_1, ..., sub^{(2)}_{n_2}, ..., sub^{(k)}_1, ..., sub^{(k)}_{n_k}]\)
\(n_1 = n_2 = ... = n_{k-1} = 2^{d_{sub}}\)
\(n_k = n - (2^{d_{sub}}\times (k - 1))\)
Using the chunks, construct sub-trees and calculate their roots as
\(\mathsf{subRoots} = [\mathsf{root}_{sub^{(1)}}, ..., \mathsf{root}_{sub^{(k)}}]\)
where
\((\mathsf{root}_{sub^{(i)}}, ,) = \mathsf{SubTree.batchAppend}(\mathsf{SubTree.initialRoot}, 0, \mathsf{SubTree.initialSiblings}, \mathsf{chunks}_{i})\)
Finally, we can define the subtree appending as
\[ \mathsf{(root_{next}, index_{next}, siblings_{next})} = \mathsf{subTreeAppend}(d_{sub}, \mathsf{root_{prev}, index_{prev}, siblings_{prev}, items}) \]
where
\(\mathsf{(root_{next}, index_{next}, siblings_{next}) = ParentTree.batchAppend(root_{prev}, index_{prev}, siblings_{prev}, subRoots)}\)
Let \(C_{utxo-tree-depth} = 48\)
Let \(C_{utxo-subtree-depth} = 5\)
Let \(\mathsf{poseidon_n}\) be defined in [1.4]
Let \(\mathsf{SMT}\) be defined in [5.1.1]
Let \(\mathsf{subTreeAppend}\) be defined in [5.2.3]
\(\mathsf{Tree_{utxo}} = \mathsf{SMT}(C_{utxo-tree-depth}, \mathsf{poseidon_2}, 0)\) is a Sparse Merkle Tree that stores newly created UTXOs using \(\mathsf{subTreeAppend}\) with \(d_{sub} = C_{utxo-subtree-depth}\).
Let \(C_{withdrawal-tree-depth} = 48\)
Let \(C_{withdrawal-subtree-depth} = 5\)
Let \(\mathsf{keccak256_2}\) be defined in [4.2.2.1]
Let \(\mathsf{Tree}\) be defined in [5.1.1]
Let \(\mathsf{subTreeAppend}\) be defined in [5.2.3]
\(\mathsf{Tree_{withdrawal}} = \mathsf{SMT}(C_{withdrawal-tree-depth}, \mathsf{poseidon_2}, 0)\) is a Sparse Merkle Tree that stores newly created Withdrawals using \(\mathsf{subTreeAppend}\) with \(d_{sub} = C_{withdrawal-subtree-depth}\).
Let \(C_{nullifier-tree-depth} = 254\)
Let \(\mathsf{keccak256_2}\) be defined in [4.2.2.1]
Let \(\mathsf{Tree}\) be defined in [5.1.1]
Let \(p\) be defined in [1.1]
\(\mathsf{Tree_{nullifier}} = \mathsf{SMT}(C_{nullifier-tree-depth}, \mathsf{keccak256_2}, 0)\) is a Sparse Merkle Tree that stores \(1\) into the leaf node where its \(\mathsf{pos} = nullifier\) to mark that \(nullifier\) as included in the tree.
To cover all possible nullifiers the number of items of the nullifier tree \(\mathsf{Tree_{nullifier}} = 2^{C_{nullifier-tree-depth}} >= p\)
The \(n\)-th Zkopru layer 2 block is denoted as \(\mathsf{Block}^{(n)} = (\mathsf{Header}^{(n)}, \mathsf{Body}^{(n)})\).
Let \(\mathsf{Header}^{(n)}\) be the header of \(n\)-th block and consists of
| Symbol | Description |
| –––– | –––– | –––– |–––– |
| \(\mathsf{proposer}^{(n)}\) | Ethereum address of the proposer of this block |
| \(\mathsf{parent}^{(n)}\) | The hash value of its parent block |
| \(\mathsf{fee}^{(n)}\) | Fee for the block proposer |
| \(\mathsf{root}^{(n)}_\mathsf{utxo}\) | \(\mathsf{root}\) of the updated \(\mathsf{Tree_{utxo}}\)|
| \(\mathsf{index}^{(n)}_\mathsf{utxo}\) | Starting position of new leaf of \(\mathsf{Tree_{utxo}}\) |
| \(\mathsf{root}^{(n)}_\mathsf{nullifier}\) | \(\mathsf{root}\) of the updated \(\mathsf{Tree_{nullifier}}\)|
| \(\mathsf{root}^{(n)}_\mathsf{withdrawal}\) | \(\mathsf{root}\) of the updated \(\mathsf{Tree_{withdrawal}}\)|
| \(\mathsf{index}^{(n)}_\mathsf{withdrawal}\) | Starting position of new leaf of \(\mathsf{Tree_{withdrawal}}\) |
| \(\mathsf{txRoot}^{(n)}\) | Merkle root of transaction hashes|
| \(\mathsf{depositRoot}^{(n)}\) | Merkle root of mass deposit hashes|
| \(\mathsf{migrationRoot}^{(n)}\) | Merkle root of mass migration hashes|
Let \(\mathsf{Body}^{(n)}\) be the body of \(n\)-th block and consists of
Symbol | Description |
---|---|
\(\mathrm{TXs}^{(n)}\) | \(=[\mathcal{Tx}_1^{(n)}, ..., \mathcal{Tx}_{n_{tx}}^{(n)}]\). Array of transactions. |
\(\mathrm{MDs}^{(n)}\) | \(=[\mathrm{MD}_1^{(n)}, ..., \mathrm{MD}_{n_{md}}^{(n)}]\). Array of mass deposits. |
\(\mathrm{MMs}^{(n)}\) | \(=[\mathrm{MM}_1^{(n)}, ..., \mathrm{MM}_{n_{mm}}^{(n)}]\). Array of mass migrations. |
Where
\(\mathcal{Tx}_i^{(n)}\) is the$ i$-th transaction of the \(n\)-th block.
\(\mathrm{MD}_i^{(n)}\) is the \(i\)-th mass deposit of the \(n\)-th block.
\(\mathrm{MM}_i^{(n)}\) is the \(i\)-th mass migration of the \(n\)-th block.
Let \(\mathsf{proposer}^{(n)}\), \(\mathsf{parent}^{(n)}\), \(\mathsf{fee}^{(n)}\), \(\mathsf{root}^{(n)}_\mathsf{utxo}\), \(\mathsf{index}^{(n)}_\mathsf{utxo}\), \(\mathsf{root}^{(n)}_\mathsf{withdrawal}\), \(\mathsf{index}^{(n)}_\mathsf{withdrawal}\), \(\mathsf{root}^{(n)}_\mathsf{nullifier}\), \(\mathsf{txRoot}^{(n)}\), \(\mathsf{depositRoot}^{(n)}\), and \(\mathsf{migrationRoot}^{(n)}\) be defined in [6.1.2]
Then, we can define their serialized form as below:
| Value | Serialized | Serialization |
| –––– | –––– | –––– |–––– |
| \(\mathsf{proposer}^{(n)}\) | \(\mathtt{proposer}^{(n)}\) |20 bytes / big-endian |
| \(\mathsf{parent}^{(n)}\) | \(\mathtt{parent}^{(n)}\) | 32 bytes / big-endian |
| \(\mathsf{fee}^{(n)}\) | \(\mathtt{fee}^{(n)}\) | 32 bytes / big-endian |
| \(\mathsf{root}^{(n)}_\mathsf{utxo}\) | \(\mathtt{root}^{(n)}_\mathsf{utxo}\) | 32 bytes / big-endian |
| \(\mathsf{index}^{(n)}_\mathsf{utxo}\) | \(\mathtt{index}^{(n)}_\mathsf{utxo}\) | 32 bytes / big-endian |
| \(\mathsf{root}^{(n)}_\mathsf{nullifier}\) | \(\mathtt{root}^{(n)}_\mathsf{nullifier}\) | 32 bytes / big-endian |
| \(\mathsf{root}^{(n)}_\mathsf{withdrawal}\) | \(\mathtt{root}^{(n)}_\mathsf{withdrawal}\) | 32 bytes / big-endian |
| \(\mathsf{index}^{(n)}_\mathsf{withdrawal}\) | \(\mathtt{index}^{(n)}_\mathsf{withdrawal}\) | 32 bytes / big-endian |
| \(\mathsf{txRoot}^{(n)}\) | \(\mathtt{txRoot}^{(n)}\) | 32 bytes / big-endian |
| \(\mathsf{depositRoot}^{(n)}\) | \(\mathtt{depositRoot}^{(n)}\) | 32 bytes / big-endian |
| \(\mathsf{migrationRoot}^{(n)}\) | \(\mathtt{migrationRoot}^{(n)}\) | 32 bytes / big-endian |
Then \(\mathtt{Header}^{(n)}\), the serialized form of \(\mathsf{Header}^{(n)}\), is the concatenation of \((\mathsf{proposer}^{(n)}, \mathsf{parent}^{(n)}, \mathsf{fee}^{(n)}, \mathsf{root}^{(n)}_\mathsf{utxo}, \mathsf{index}^{(n)}_\mathsf{utxo}, \mathsf{root}^{(n)}_\mathsf{nullifier}, \mathsf{root}^{(n)}_\mathsf{withdrawal}, \\ \mathsf{index}^{(n)}_\mathsf{withdrawal}, \mathsf{txRoot}^{(n)}, \mathsf{depositRoot}^{(n)}, \mathsf{migrationRoot}^{(n)})\)
Prepare a dynamic sized buffer, \(\mathtt{buff}_{tx_{i}}\). We will push bytes data to the buffer by the following sequences. The final state of \(\mathtt{buff}_{tx_{i}}\) after appending all data becomes \(\mathtt{Tx}_i\).
Let \(m\) be the number of input notes that is defined in [3.4.1].
Store \(m\) into a single byte and push it to \(\mathtt{buff}_{tx_{i}}\).
Let \(\mathcal{In}_i\) be the \(i\)-th input note of \(\mathcal{Tx}\) as it's defined in [3.4.1]
Let \(m\) be the number of input notes that is defined in [3.4.1].
Starting from \(i\) = 1 and repeat the below steps until \(i\) is less than or equal to \(m\):
Let \(n\) be the number of output notes that is defined in [3.4.1].
Store \(n\) into a single byte and push it to \(\mathtt{buff}_{tx_{i}}\).
Let \(\mathcal{Out}_i\) be the \(i\)-th output note of \(\mathcal{Tx}\) as it's defined in [3.4.1]
Let \(n\) be the number of output notes that is defined in [3.4.1].
Starting from \(i\) = 1 and repeat the below steps until \(i\) is less than or equal to \(n\):
Let \(f\) be the transaction fee for \(\mathcal{Tx}\) as defined in [3.2.6].
Serialize \(f\) into a 32 bytes buffer with big-endian, and push it to \(\mathtt{buff}_{tx_i}\).
Let \(\pi = (\mathbf{A}, \mathbf{B}, \mathbf{C})\) be defined in [3.2.5].
Serialized form of transaction can have some extra data. Zkopru expresses the existence of extra data using 2 bits.
Freeze the dynamic sized buffer \(\mathtt{buff}_{tx_i}\) and let it be \(\mathtt{TX}_i\)
Let \(\mathrm{MD}_i\) be the \(i\)-th Mass Deposit of \(\mathsf{Block}^{(n)}\).
Let \(\mathrm{MM}_i\) be the \(i\)-th Mass Migration of \(\mathsf{Block}^{(n)}\) and \(\mathrm{MM} = (\mathsf{dest}, \mathsf{asset_{migration}}, \mathrm{MD}_\mathsf{migration})\) as defined in [4.4.1].
Then, let \((\mathsf{eth, token, amount}) = \mathsf{asset_{migration}}\) and \((\mathsf{merged}, f_\mathrm{MD}) = \mathrm{MD}_\mathsf{migration}\)
Prepare a dynamic sized buffer, \(\mathtt{buff}_{body}\). We will push bytes data to the buffer by the following sequences. The final state of \(\mathtt{buff}_{body}\) after appending all data becomes \(\mathtt{Body}^{(n)}\).
Let \(\mathsf{Block}^{(n)}\) has \(n_{tx}\) number of transactions.
Let \(\mathsf{Block}^{(n)}\) has \(n_{md}\) number of mass deposits.
Let \(\mathsf{Block}^{(n)}\) has \(n_{mm}\) number of mass migrations.
\(\mathtt{Body}^{(n)}\) is the serialized form of \(\mathsf{Body}^{(n)}\) and consists of
Symbol | Description |
---|---|
\(\mathtt{TXs}^{(n)}\) | \(=[\mathtt{TX}_1^{(n)}, ..., \mathtt{TX}_{n_{tx}}^{(n)}]\) |
\(\mathtt{MDs}^{(n)}\) | \(=[\mathtt{MD}_1^{(n)}, ..., \mathtt{MD}_{n_{md}}^{(n)}]\) |
\(\mathtt{MMs}^{(n)}\) | \(=[\mathtt{MM}_1^{(n)}, ..., \mathtt{MM}_{n_{mm}}^{(n)}]\) |
Where
\(\mathtt{TX}_i^{(n)}\) is the serialized form of \(\mathcal{Tx}_i\), the \(i\)-th transaction of the \(n\)-th block.
\(\mathtt{MD}_i^{(n)}\) is the serialized form of \(\mathrm{MD}_i\), the \(i\)-th mass deposit of the \(n\)-th block.
\(\mathtt{MM}_i^{(n)}\) is the serialized form of \(\mathrm{MM}_i\), the \(i\)-th mass migration of the \(n\)-th block.
Freeze the dynamic sized buffer \(\mathtt{buff}_{body}\) and let it be \(\mathtt{Body}^{(n)}\)
Let \(\mathtt{Header}^{(n)}\) be defined in [6.2.1].
Let \(\mathtt{Body}^{(n)}\) be defined in [6.6.5].
Then, \(\mathtt{Block}^{(n)}\) is the serialized form of \(\mathsf{Block}^{(n)}\) and equals the concatenation of \(\mathtt{Header}^{(n)}\) and \(\mathtt{Body}^{(n)}\).
To finalize a block proposal, it requires the header data, and the mass deposits and mass migrations.
Let \(\mathtt{Header}^{(n)}\) be defined in [6.2.1].
Let \(\mathtt{MDs}^{(n)}\) be defined in [6.6.5].
Let \(\mathtt{MMs}^{(n)}\) be defined in [6.6.5].
First, compute the data checksum of original block data using keccak256:
\[
\mathsf{checksum} = keccak256(\mathtt{Block}^{(n)})
\]
Then, \(\mathtt{Finalization}^{(n)}\) is the concatenation of \((\mathsf{checksum}, \mathtt{Header}^{(n)}, \mathtt{MDs}^{(n)}, \mathtt{MMs}^{(n)})\).
\(\mathsf{MerkleRoot_{h}([item_1, ...., item_n])}\) means the root of a hash tree that uses \(\mathsf{h}\) for its hash function.