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,
Let
The Montgomerry form of the Baby Jubjub curve
$$
E_M: v^2 = u^3 + 168698u^2 + u
$$
The order of
Let
Denote
Let
The Edward form of the curve
If
If
Let
$$
y = \mathsf{poseidon_n}([x_1, x_2, ..., x_n]) \
$$
Then,
$$
x_i, y \in \mathbb{F}_p\
$$
Where
The number of rounds of
2 | 3 | 8 | 57 |
3 | 4 | 8 | 56 |
4 | 5 | 8 | 60 |
The implementation that Zkopru uses are
- Javascript: https://github.com/iden3/circomlib/blob/cf853c1cc96fa537cb1030f70a6f78e5d80ed0e4/src/poseidon.js
- Circuit: https://github.com/iden3/circomlib/blob/cf853c1cc96fa537cb1030f70a6f78e5d80ed0e4/circuits/poseidon.circom
Let
Let
- Hash the 32-byte private key
$s$ using$\mathsf{blake512}$ , storing the digest in a 64-octet large buffer, denoted h. Only the lower 32 bytes are used for generate the scalar multiplier$a$ . - Prune the buffer: Discard the lowest three bits of the first octet and the highest bit of the last octet. And set the second highest bit of the last octet.
- Interpret the buffer as the little-endian integer and modularize with
$r$ , forming a secret scalar$a$ .
Let
Let
Let
Let
Let
- Prepare a 512 bits buffer.
- Store
$\mathsf{Pub_{sk}}$ to the first 256 bits in little-endian mode. - Pack
$\mathbf{V}$ into 256 bits data. :::info Point encode/decode protocol: https://tools.ietf.org/html/rfc8032#section-5.1.2 :::- Let
$\mathbf{V} = (x,y)$ - Prepare a 256 bits buffer.
- Store
$y$ into the buffer in little-endian mode. - If
$x$ is an odd number fill the most significant bit of the last octet.
- Let
- Store the packed
$\mathbf{V}$ to the last 256 bits. - Digest the prepared buffer with keccak256 hash function and take the first 32 bits from the resulting hash value as the checksum data.
- Encode the concatenation of the 512 bits data and its 32 bits checksum with Base58 method.
- Finally
$\mathcal{Z}$ is the encoded result.
Let
Then
It follows the RFC8032 with blake512 hash and
- Sign
- hash the 32 bytes private key
$s$ with$\mathsf{blake512}$ hash function. Let$\mathsf{h}$ denote the resulting digest.
- hash the 32 bytes private key
for the private key hashing and the prefix generation.
:::info 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,
Zkopru note (denoted
Let
-
$\mathsf{v_{eth}}$ : The spendable amount of Ether which is less than${2^{245}}$ . -
$\mathsf{addr_{token}}$ : The token contract address if the note contains ERC20 or NFT. The value is less than${2^{160}}$ . -
$\mathsf{v_{erc20}}$ : The spendable amount of ERC20 token if the$\mathsf{addr_{token}}$ is a registered ERC20 token address on the$\mathsf{ZkopruContract}$ . Otherwise$\mathsf{v_{erc20}} = 0$ . -
$\mathsf{v_{nft}}$ : The token id of the NFT that the note owns. This is less than$p$ and$0$ means it does not own any NFT. Likewise$\mathsf{v_{erc20}}$ ,${v_{nft}}$ can be non-zero when only$\mathsf{addr_{token}}$ is a registered ERC721 token address on the$\mathsf{ZkopruContract}$ . :::warning NFTs with ID 0 cannot be deposited on the$\mathsf{ZkopruContract}$ . :: :
Let
Let
:::info
Please note that the information written in
Let
The transaction output of Zkopru $$ \mathrm{Out} = (\mathsf{N}, t, \mathcal{N}) $$
where
$\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
Let
Then,
Let
Then,
where
Position | Size | Data |
---|---|---|
0-32 | 32 bytes | |
32-52 | 20 bytes | |
52-84 | 32 bytes | |
84-104 | 20 bytes | |
104-136 | 32 bytes | |
136-168 | 32 bytes | |
168-200 | 32 bytes |
Let
Let
Let
Then the nullifier that prevents double spending is computed by:
Let
Let the root of $\mathsf{Tree}\mathsf{utxo}$ at the $n$-th layer 2 block be denoted $\mathsf{root{utxo}}^{(n)}$.
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
The confidential data of the
where
- $\mathsf{poseidon}3(x, y, v) == \mathsf{Pub{sk}}$ where
$\mathbf{A} = (x, y)$ -
$\mathsf{hash}(\mathrm{U}), \mathbf{A}, \mathbf{R}, S$ satisfies [1.14] -
$\mathsf{nullifier}(\mathrm{U})$ satisfies [3.1.6]
Then, its public inflow data is defined as $$ \mathcal{In} = (\mathsf{nullifier}(\mathrm{U}), \mathsf{root_{utxo}}^{(n)}) $$
Let
Then,
while
Let
Then, a raw transaction
where
Let
$\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
Let
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
Each
Let
Each
Let
Each
Let
Then,
Let
Then,
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
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
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
Let's define a fillter function
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
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
Let
Then for each
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
Let
Then the prover can generate the SNARK proof
Let $\mathsf{C}{(x,y)}$ be defined in [3.2.1].
Let $\mathsf{zVK}{(x,y)}$ be defined in [3.2.3].
Let
Then define the public signal set
then verifiers can verify the zero-knowledge proof using
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
-
The transaction builder pick 1 output
$\mathrm{Out}$ from$\mathrm{Tx}$ to include in the memo. -
Generate a public shared key using ECDH
- Get the viewing public key
$\mathbf{V}_{}$ by parsing the zk address of$\mathrm{Out}$ as defined in [1.13] - Create a random 16 bytes ephemeral secret key
$e$ - Generate the public ephemeral key
$\mathbf{E}$ where$\mathbf{E} = e \cdot \mathbf{B}$ . - Generate the shared public key
$\mathbf{S}$ where$\mathbf{S} = e \cdot \mathbf{V} = v \cdot \mathbf{E}$ where$v$ is the secret viewing key of$\mathrm{Out}$ owner. - The encryption key
$\mathsf{key} = \mathsf{encode(\mathbf{S})}$
- Get the viewing public key
-
Prepare the data to encrypt
- Prepare a 49 bytes buffer
- Store
$\mathsf{salt}$ to the first 16 bytes with big-endian. - Get the 1 byte
$\mathsf{tokenId(addr_{token})}$ of$\mathrm{Out}$ and store it to the 17-th byte. -
$\mathsf{v}$ is one of$\mathsf{v_{eth}}$ or$\mathsf{v_{erc20}}$ or$\mathsf{v_{nft}}$ . Store$\mathsf{v}$ into the 18-th bytes with big-endian.
-
Run encryption using the shared key with chacha20
$\mathsf{secret} = \mathsf{(salt, tokenId, v)}$ $\mathsf{ciphertext} \leftarrow \mathsf{chacha20(secret, key)}$ -
Pack
- Prepare 81 bytes buffer
- Store
$\mathsf{encode(\mathbf{E})}$ to the first 32 bytes. - Store the encrypted 49 bytes of cipher text to the 33-rd byte and complete the 81 bytes memo field. $$ \mathsf{memo} = (\mathsf{encode}(\mathbf{E}), \mathsf{ciphertext}) $$
To receive Zkopru note, recipient should try to decrypt memo field to find own notes.
Let
- Parse the first 31 bytes of public ephemeral key data and decode it to
$\mathbf{E}$ - Compute the scalar multiplication with the viewing key
$v$ and get the shared public key$\mathbf{S}$ . - Encode the public shared key
$S$ and get the chacha20 cipher$\mathsf{key}$ - Parse the remaining 49 bytes of the memo field and decrypt the value using
$\mathsf{key}$ and get$\mathsf{secret' = (salt', tokenId', v')}$ . Please note that we're not sure$\mathsf{secret}'$ is a correctly decrypted one. - Fetch registered token addresses from the layer 1 smart contract. And filter them and get addresses which token id is same with
$\mathsf{tokenId'}$ - For each token addresses, try to construct a output note with 3 cases:
a.
$\mathsf{v_{eth} = v', v_{erc20} = 0, v_{nft} = 0}$ b.$\mathsf{v_{eth} = 0, v_{erc20} = v', v_{nft} = 0}$ c.$\mathsf{v_{eth} = 0, v_{erc20} = 0, v_{nft} = v'}$ - Compute the contructed output note's hash value and compare them to $\mathcal{Out}$s.
- If it succeeds to find the exact same hash, then the decryption suceeds. Or the transaction is not containing a proper memo field for that viewing key owner.
Let
Then, the shielded transaction
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 |
---|---|---|
48 | UTXO tree depth. It affects the SNARK proving time. | |
281474976710656 | We can use this tree for about 45000 years when we have 100 TPS with 2 output notes for 1 transaction. | |
48 | UTXO tree depth. | |
281474976710656 | We can use this tree for about 90000 years when we have 100 TPS with 1 withdrawal note for 1 transaction. | |
254 | Nullifier tree's depth is 254. | |
5 | A UTXO subtree's depth is 5. | |
32 | A UTXO subtree has 32 leaves. | |
5 | A Withdrawal subtree's depth is 5. | |
32 | A withdrawal subtree has 32 leaves. | |
200000 | Block should not be too large. Unit in byte. | |
6000000 | Block proposer should not submit a block which validation process exceeds the given gas limit | |
46253 | Challenge period in block number unit. | |
32e18 | Minimum amount of Ether staking to propose a block. | |
128 | Recent |
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
Let's define deposit
Let's define
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
where
So we can express this
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 deposit()
updates the staged deposits
Let
Then,
$$ \mathrm{MD}{stage} = (\mathsf{merged}, f{MD}) $$
where
Finally, the deposit()
transaction transfers assets and stores the updated 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
Then, the mass deposit
where
Finally, commitMassDeposit()
stores 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
Let
Let
Let the root of $\mathsf{Tree}\mathsf{withdrawal}$ at the $n$-th layer 2 block be denoted $\mathsf{root{withdrawal}}^{(n)}$.
Then the block
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
Layer 1 transaction withdraw()
should include
Then, the submitted withdraw()
transaction executor.
As the user should wait the finalization,
For the instant withdrawal with pay in advance feature,
-
$\mathcal{W}$ owner select a prepayer and generate a message that follows the EIP712 spec with the following structure.Since thestruct PrepayRequest { address prepayer; bytes32 withdrawalHash; uint256 prepayFeeInEth; uint256 prepayFeeInToken; uint256 expiration; }
$\mathcal{W}$ might be a ERC20 only withdrawal note, the owner can choose how to pay the fee for instant withdrawal to the prepayer. - Generate a ECDSA with the correct account which address is
$\mathcal{N}.\mathsf{to}$ and send a request to the prepayer using a communication channels like HTTP. - If the request look profitable, the prepayer verifies the validity calls the
payInAdvance()
function with the received ECDSA signature and correct amount of assets to pay in advance for the original owner. - Smart contract verifies the relationship between
$\mathsf{hash(\mathcal{W})}$ , ECDSA, transaction signer and the expiration timestamp.- Computed withdrawal hash of the given details equals to
withdrawalHash
. - ECDSA satisfy EIP712 sign spec with its own domain using chain Id and contract address.
- Current block timestamp is smaller than the given expiration.
- Computed withdrawal hash of the given details equals to
- If it passes all verifications, it records the transferred ownership of the
$\mathcal{W}$ and it transfers assets to the original owner.
Let
Then, for each
Let
Then, for each
where
$\mathsf{asset_{migration}} = (\mathsf{eth, token, amount})$ - $\mathrm{MD}\mathsf{migration} = (\mathsf{merge(\mathcal{[M_1, ..., M_k]})}, \Sigma{i=0}^{k}\mathcal{M}.\mathsf{fee_{migration}})$
Let a block contain mass migrations
Then
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
Once the coordinator has proposer amount of stakes, proposer can submit a serialized form of propose(bytes calldata)
function.
To call the function propose()
, it requires
- The
msg.sender
should have staked more than 32 ETH. -
$len(\mathtt{Block}^{(n)}) < C_{max-block-size}$ . -
$\mathsf{Block}^{(n)}.\mathsf{proposer}$ equals to themsg.sender
. - There is no duplicated proposal that has same block proposal checksum which is defined in [6.7.2].
Once the function propose()
is called,
- It records the block hash by chaining with its parent hash value.
- It saves the proposal checksum for its future challenge.
- It records the utxo root for the utxo inclusion proof reference.
- It records the withdrawal root for the withdrawal proof of [4.3.1.2].
- It extends the
Zkopru.chain.proposers.exitAllowance
to its challenge due block number that isblock.number
+$C_{challenge-preiod}$. - And commit the latest staged deposits.
Coordinators can withdraw their staked ETH whenever Zkopru.chain.proposers.exitAllowance
is behind the block number.
Let
Anyone can call the finalize(bytes calldata)
function by submitting the finalization data
Using the proposal
from Zkopru.chain.proposals[checksum]
.
To finalize a block, it requires
-
$\mathsf{depositRoot}^{(n)}$ should equal to the hash of the submitted$\mathtt{MDs}^{(n)}$ . -
$\mathsf{migrationRoot}^{(n)}$ should equal to the hash of the submitted$\mathtt{MMs}^{(n)}$ . -
$\mathsf{hash}(\mathsf{Header}^{(n)})$ should equal to the stored hash inproposal
. -
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 behindblock.number
. - Every
$\mathtt{MD}$ should be committed in theZkopru.chain.committedDeposits
. -
$\mathsf{migrationRoot}^{(n)}$ should not exist in theZkopru.chain.migrationRoots
.
Once the function finalize()
is called,
- It removes the every
$\mathtt{MD}$ fromZkopru.chain.committedDeposits
. - It marks
$\mathsf{migrationRoot}^{(n)}$ as true inZkopru.chain.migrationRoots
. It allowsmigrateFrom()
function call in [4.5.6]. - It gives the
$\mathsf{fee}^{(n)}$ to the proposer. - It marks the block header hash as finalized in
Zkopru.chain.finalized
- It marks the utxo root as finalized in
Zkopru.chain.finalizedUTXORoots
- It updates the latest block hash
latest
. - It deletes
proposal
.
Let
After 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
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
Let
Then, if any of the Zkopru.chain.committedDeposits
, the validation contract returns slashable = true
with code D1.
Let
When slashable = true
with code H1.
Let
When slashable = true
with code H2.
Let
When 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, Zkopru.chain.slashed[parent]
exists, the validation contract returns slashable = true
with code H5.
Let
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
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 slashable = true
with code M2.
Let
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 slashable = true
with code M3.
Let
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
If it does not satisfy the
the validation contract returns slashable = true
with code M5.
Let
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 slashable = true
with code M7.
Let
Let
Then, for every
And appending all
Otherwise, the validation contract returns slashable = true
with code N1.
Let
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
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
Therefore $$ len(\mathsf{utxos}^{(n)}) \ mod \ C_{utxo-sub-tree-size} = 0 $$
Let
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
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
Then, appending the hash of each items in
Otherwise, the validation contract returns slashable = true
with code U3.
Let
Let
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
Therefore $$ len(\mathsf{withdrawals}^{(n)}) \ mod \ C_{withdrawal-sub-tree-size} = 0 $$
Let
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
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
Then, appending the withdrawal hash of each items in
Otherwise, the validation contract returns slashable = true
with code W3.
Let
Then for every Zkopru.chain.finalizedUTXORoots
.
Otherwise, the validation contract returns slashable = true
with code T1.
Let
Then for every
Otherwise, the validation contract returns slashable = true
with code T2.
Let
Then, for every
Otherwise, the validation contract returns slashable = true
with code T3.
Let
For every Zkopru.chain.registeredERC20s
or Zkopru.chain.registeredERC721s
.
Otherwise, the validation contract returns slashable = true
with code T4.
Let
For every Zkopru.chain.registeredERC721s
.
Otherwise, the validation contract returns slashable = true
with code T5.
Let
For every Zkopru.chain.registeredERC20s
.
Otherwise, the validation contract returns slashable = true
with code T6.
Let
For every 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
If
If there does not exist correct pair, the validation contract returns slashable = true
with code T8.
Let
Then for every
Otherwise, it is considered as a used one and the validation contract returns slashable = true
with code T9.
Let
Then for every
Otherwise, it is considered as a used one and the validation contract returns slashable = true
with code T10.
Let
Then, verifyig key for circuit 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
Then, $$ \mathsf{verify_{groth16}}(\mathcal{P}, \pi, \mathsf{vPK}_{(x,y)}) = 1 $$
Otherwise, the validation contract returns slashable = true
with code S2.
Let
Then, every value of
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
Let
Then, the tree has
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
Let
Then, it has
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.
- Leaf node
- Branch node
- Root node
Let
Every leaf node has no child and its initial value is
As
Let
Every node except leaf node is kind of the branch node, and their value is decided by the values of their children nodes.
Let
Then,
Then index of leaf node should be greater or equal than 1 and less than
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
Let
Then, we can compute the root value using the
where
Using
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
Let
Then we can define the
where
Let
Then, we can define
where
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
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
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}]$
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
where
Let
Let
Let
To cover all possible nullifiers the number of items of the nullifier tree
The
Let
| Symbol | Description |
| -------- | -------- | -------- |-------- |
|
Let
Symbol | Description |
---|---|
$=[\mathcal{Tx}1^{(n)}, ..., \mathcal{Tx}{n_{tx}}^{(n)}]$. Array of transactions. | |
$=[\mathrm{MD}1^{(n)}, ..., \mathrm{MD}{n_{md}}^{(n)}]$. Array of mass deposits. | |
$=[\mathrm{MM}1^{(n)}, ..., \mathrm{MM}{n_{mm}}^{(n)}]$. Array of mass migrations. |
Where
Let
Then, we can define their serialized form as below:
| Value | Serialized | Serialization |
| -------- | -------- | -------- |-------- |
|
Then
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
Let
Let
Starting from
- Let
$(\mathsf{nullifier}(\mathrm{U}), \mathsf{root_{utxo}}^{(n)}) = \mathcal{In}_i$ - Serialize
$\mathsf{nullifier}(\mathrm{U})$ into 32 bytes buffer with big-endian. - Serialize
$\mathsf{root_{utxo}}^{(n)}$ in to 32 bytes buffer with big-endian. - Concatenate the serialized
$\mathsf{nullifier}(\mathrm{U})$ and$\mathsf{root_{utxo}}^{(n)}$ into 64 bytes buffer and let it be$\mathtt{In}_i$ - Push $\mathtt{In}i$ to $\mathtt{buff}{tx_{i}}$
Let
Let
Starting from
- Let $(\mathsf{hash}(\mathsf{N}), t, \mathcal{N}) = \mathcal{Out}i$ and $\mathcal{N} = \mathsf{(to, v'{eth}, addr'{token}, v'{erc20}, v'{nft}, fee{L1})}$ as defined in [3.1.1]
- Prepare a dynamic sized buffer
$\mathtt{buff}_{out_i}$ . - Serialize
$\mathsf{hash}(\mathsf{N})$ into 32 bytes data with big-endian and push them to$\mathtt{buff}_{out_i}$ . - Store
$t$ into a single byte with big-endian and push it to$\mathtt{buff}_{out_i}$ . - If
$t$ is not zero,- Prepare 168 bytes buffer
$\mathtt{buff}_{public_i}$ - Serialize
$\mathsf{to}$ to a 20 bytes buffer with big-endian. - Serialize
$\mathsf{v'_{eth}}$ to a 32 bytes data with big-endian. - Serialize
$\mathsf{addr'_{token}}$ to a 20 bytes data with big-endian. - Serialize
$\mathsf{v'_{erc20}}$ to a 32 bytes data with big-endian. - Serialize
$\mathsf{v'_{nft}}$ to a 32 bytes data with big-endian. - Serialize
$\mathsf{fee_{L1}}$ to a 32 bytes data with big-endian. - Concatenate the serialized data and store them to
$\mathtt{buff}_{public_i}$ . - Push $\mathtt{buff}{public_i}$ to $\mathtt{buff}{out_i}$
- Prepare 168 bytes buffer
- Push $\mathtt{buff}{out_i}$ to $\mathtt{buff}{tx_i}$
Let
Let
- Prepare 256 bytes empty buffer
$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{A}.x$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{A}.y$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{B}.{x_1}$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{B}.{x_2}$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{B}.{y_1}$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{B}.{y_2}$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{C}.x$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Serialize
$\mathbf{C}.y$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{snark}$ . - Push $\mathtt{buff}{snark}$ to $\mathtt{buff}{tx_i}$.
Serialized form of transaction can have some extra data. Zkopru expresses the existence of extra data using 2 bits.
- Prepare a single byte
$\mathtt{b}$ . - Let
$\mathsf{swap}$ be defined in [3.2.6]. If$\mathsf{swap}$ is not zero, store 1 on its right bit position. - If
$\mathtt{Tx}$ has a memo field, store 1 on its second right bit position. - Push the byte
$\mathtt{b}$ into$\mathtt{buff}_{tx_i}$ - If the right most bit of
$\mathtt{b}$ is 1, serialize$\mathsf{swap}$ to a 32 bytes buffer with big-endian and push it to$\mathtt{buff}_{tx_i}$ . - Let
$\mathsf{memo}$ be defined in [3.3.1]. If the second right most bit of$\mathtt{b}$ is 1, push that 81 bytes$\mathsf{memo}$ data to$\mathtt{buff}_{tx_i}$
Freeze the dynamic sized buffer
Let
- Prepare a 64 bytes buffer
$\mathtt{buff}_{md_i}$ . - Let
$(\mathsf{merged}, f_{MD}) = \mathrm{MD}_i$ as defined in [4.2.2.3] - Serialize
$\mathsf{merged}$ into 32 bytes buffer with big-endian. - Serialize
$f_{MD}$ into 32 bytes buffer with big-endian. - Concatenate the serialized
$\mathsf{merged}$ and$f_{MD}$ into$\mathtt{buff}_{md_i}$ and let it be$\mathtt{MD}_i$
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
- Prepare 168 bytes buffer $\mathtt{buff}{mm{i}}$.
- Serialize
$\mathsf{dest}$ into 20 bytes buffer with big-endian and push it into$\mathtt{buff}_{mm_i}$ . - Serialize
$\mathsf{eth}$ into 32 bytes buffer with big-endian and push it into$\mathtt{buff}_{mm_i}$ . - Serialize
$\mathsf{token}$ into 20 bytes buffer with big-endian and push it into$\mathtt{buff}_{mm_i}$ . - Serialize
$\mathsf{amount}$ into 32 bytes buffer with big-endian and push it into$\mathtt{buff}_{mm_i}$ . - Serialize
$\mathsf{migration}.\mathsf{merged}$ into 32 bytes buffer with big-endian and push it into$\mathtt{buff}_{mm_i}$ . - Serialize
$f_\mathsf{MD}$ into 32 bytes buffer with big-endian and push it into$\mathtt{buff}_{mm_i}$ . - Finally set
$\mathtt{buff}_{mm_i}$ as$\mathtt{MM}_i$
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
Let
- Prepare a dynamic sized buffer
$\mathtt{buff}_{tx}$ to store all serialized transactions. - Store
$n_{tx}$ into a single byte with big-endian and push it into$\mathtt{buff}_{tx}$ . - Starting from
$i$ = 1 and repeat the below steps until$i$ is less than or equal to$n_{tx}$ :- Serialize
$\mathcal{Tx}_i$ to$\mathtt{Tx}_i$ as defined in [6.3.9] - Push $\mathtt{Tx}i$ to $\mathtt{buff}{tx}$.
- Serialize
- Let $\mathtt{buff}{tx}$ be $\mathtt{TXs}^{(n)}$ and push it to $\mathtt{buff}{body}$
Let
- Prepare a dynamic sized buffer
$\mathtt{buff}_{md}$ to store all serialized mass deposits. - Store
$n_{md}$ into a single byte with big-endian and push it into$\mathtt{buff}_{md}$ . - Starting from
$i$ = 1 and repeat the below steps until$i$ is less than or equal to$n_{md}$ :- Serialize
$\mathrm{MD}_i$ to$\mathtt{MD}_i$ as defined in [6.4.1] - Push $\mathtt{MD}i$ to $\mathtt{buff}{md}$.
- Serialize
- Let $\mathtt{buff}{md}$ be $\mathtt{MDs}^{(n)}$ and push it to $\mathtt{buff}{body}$
Let
- Prepare a dynamic sized buffer
$\mathtt{buff}_{mm}$ to store all serialized transactions. - Store
$n_{mm}$ into a single byte with big-endian and push it into$\mathtt{buff}_{mm}$ . - Starting from
$i$ = 1 and repeat the below steps until$i$ is less than or equal to$n_{mm}$ :- Serialize
$\mathrm{MM}_i$ to$\mathtt{MM}_i$ as defined in [6.3.9] - Push $\mathtt{MM}i$ to $\mathtt{buff}{mm}$.
- Serialize
- Let $\mathtt{buff}{mm}$ be $\mathtt{MMs}^{(n)}$ and push it to $\mathtt{buff}{body}$
Symbol | Description |
---|---|
$=[\mathtt{TX}1^{(n)}, ..., \mathtt{TX}{n_{tx}}^{(n)}]$ | |
$=[\mathtt{MD}1^{(n)}, ..., \mathtt{MD}{n_{md}}^{(n)}]$ | |
$=[\mathtt{MM}1^{(n)}, ..., \mathtt{MM}{n_{mm}}^{(n)}]$ |
Where
Freeze the dynamic sized buffer
Let
Then,
To finalize a block proposal, it requires the header data, and the mass deposits and mass migrations.
Let
First, compute the data checksum of original block data using keccak256: $$ \mathsf{checksum} = keccak256(\mathtt{Block}^{(n)}) $$
Then,