The main data silo of RollupNC. Roughly analogous to the blockchain network in L1
The prevailing data structure throughout the roll-up is a single merkle tree representing the entire L2 network state. That is, the merkle root represents a set of "account" leaves inserted into an incremental merkle tree.
Account Leaves
In order to represent an abstraction of data in our merkle tree (i.e. a layer 2 account), we need a standardized 'type' that we can use to define or encode the data stored in the merkle tree. A given account is represented as:
// pseudocode
type account = {
pubkey[2], // the x and y coordinate points representing the EdDSA pubkey
balance, // the balance of this account (in tokenType tokens)
nonce, // the number of transactions this account has made
tokenType // the id of the token as registered in TokenRegistry.sol
}
In order to turn an account into an account leaf, we simply apply the Poseidon hash function with 5 inputs.
JavaScript
We can use the circomlibjs sibling function to the circomlib Poseidon hash to generate the data we need in JavaScript:
In the smart contract, account leaves are only generated for deposits. At all other points the L1 verification of account leaves happens in Zero Knowledge. The generation of account leaves is shown here:
function deposit(
uint256[2] memory pubkey,
uint256 amount,
uint256 tokenType
) public payable {
// Ensure token can be transferred
checkToken(amount, tokenType);
// Store deposit leaf
uint256 depositHash = PoseidonT6.poseidon(
[pubkey[0], pubkey[1], amount, uint256(0), tokenType]
);
pendingDeposits[depositQueueEnd] = depositHash;
.
.
.
}
Zero Knowledge
In the Zero Knowledge circuit, there is a specialized template/ component made for verifying the integrity of an account leaf with a Poseidon Hash: balance_leaf.circom.
pragma circom 2.0.3;
include "../../../node_modules/circomlib/circuits/poseidon.circom";
template BalanceLeaf() {
signal input pubkey[2]; // Account EdDSA pubkey [x, y]
signal input balance; // Account token balance
signal input nonce; // Number of transactions made by the account
signal input tokenType; // Token registry index for token type
signal output out;
component balanceLeaf = Poseidon(5);
balanceLeaf.inputs[0] <== pubkey[0];
balanceLeaf.inputs[1] <== pubkey[1];
balanceLeaf.inputs[2] <== balance;
balanceLeaf.inputs[3] <== nonce;
balanceLeaf.inputs[4] <== tokenType;
out <== balanceLeaf.out;
}
Proof of Account Leaf Inclusion in Tree
This section will cover the basic of account leaf inclusion proof. The application of this proof to the effect of the Rollup is further discussed in Layer 1 Deposits to Layer 2. It will also be similar to the Transaction Leaf Inclusion Tree. We will be incrementally adding entries to an empty binary merkle tree as accounts enter L2. At any given point, proving inclusion in the on-chain state root confers the integrity of your L2 balance as encoded above.
The first thing to note is the tree depth - this is specified in update_state_verifier.circom as a main component's template parameter:
pragma circom 2.0.3;
.
.
.
// the first parameter `balDepth` specifies state tree depth
template Main(balDepth, txDepth) {
.
.
.
}
// state tree depth set to 4
component main { public [txRoot, prevRoot, nextRoot] } = Main(4, 2);
For the purpose of succinct testing, we set a very small state tree depth of 4. Binary merkle trees have a size of 2^n where n is the depth of the tree, meaning our state tree can hold 16 unique accounts. For production grade L2 networks, in 2020 Loopring used a tree depth of 24 and decided to decrease to a depth of 20 (pull req).
pragma circom 2.0.3;
include "./get_merkle_root.circom";
template LeafExistence(depth){
// k: tree depth
// Constrain proof to a given leaf that can be proven to exist in the given root
signal input leaf;
signal input root;
signal input proof[depth];
signal input positions[depth];
component computedRoot = GetMerkleRoot(depth);
computedRoot.leaf <== leaf;
for (var i = 0; i < depth; i++){
computedRoot.proof[i] <== proof[i];
computedRoot.positions[i] <== positions[i];
}
// equality constraint
root === computedRoot.out;
}
As expanded upon in Layer 2 Transacting, update_state_verifier.circom chains multiple balance existence checks together to perform the verifiable computation of transitions between state roots.
Priming/ Instantiation of the Account Tree
In order to operate the roll-up, two accounts must be added at instantation:
Zero Address (burning and withdrawing)
Sequencer Account (permissioned)
Reiterated from the JavaScript above, this can be shown as:
These two deposits can be batched instantly into the contract state (see Layer 1 Deposits to Layer 2) instantly; however the next batch will be limited to depth of 1 or 2.