Revised 12-03-2021
Ariel Gabizon | Zac Williamson | Tom Walton-Pocock
This document provides details on the protocol specification for Aztec 2.0, deployed to Ethereum mainnet, verifier address 0x737901bea3eeb88459df9ef1BE8fF3Ae1B42A2ba, on 15 March 2021.
The protocol is principally designed by Zac Williamson, and built on work by Zac and Ariel Gabizon, the creators of Aztec 2.0's cryptosystems PLONK and Plookup.
Aztec 2.0 is a multi-asset private rollup service on Ethereum.
The protocol supports transactions in up to 3 ERC-20 assets, as well as ETH. The protocol will be modified to support all ERC20s in the future.
The protocol consists of:
Aztec uses the Ethereum-native version of the BN254 elliptic curve for its principal group:
A BN curve of size , with field size , and security of roughly 110 bits (practically, this can be closer to 128 bits as the stronger attacks require unproven assumptions related to number field sive algorithms and have never been fully specified or implemented).
We have .
As usual, we use a subgroup of a twist of the above curve for efficient pairings:
A subgroup of size , of a curve over field size . This is a degree-2 field extension of , via . Note that is the ideal generated by , whose roots are .
We use the Ethereum-native Ate pairing, a bilinear map taking:
Where is a field extension of of degree 12.
Further details may be found here.
Grumpkin is in fact a curve cycle together with BN-254, meaning that the field and group order of Grumpkin are equal to the group and field order of (the first pairing group of) BN-254:
The Aztec 2.0 system relies on two types of hashes:
Aztec relies overwhelmingly on Pedersen Hashes; as most of the time collision resistance is sufficient.
Let be an additive group of prime order .
In its classical setting a pedersen hash is defined as a map as follows:
for generators chosen independently by public randomness (e.g. hueristically as distinct outputs of a random oracle simulating hash function).
We wish to define a variant of Pedersen to enable hashing strings of any desired length. As our group we will use the Grumpkin curve group described above.
We generate a sequence of generators as hash outputs – these are network parameters, fixed for the life of the protocol. They are simply chosen to be the first Keccak-256 outputs that correspond to group elements. See the derive_generators
method in the barretenberg code and the Global Constants section below for exact details.
Our basic component for hashing will be the hash_single
method.
Given a field element and hash index , we essentially hash 252 bits of with and the the remaining 2 bits of with . This is not precisely the case, as we use a wnaf representation - see page 4 here. See the comments above hash_single
in the code for exact details. The point is that while enforcing the wnaf representation to represent an integer smaller than , this is a collision resistant function from under DL, even when outputting only the -coordinate.
Now, given a vector we define the pedersen hash as
Given a message of arbitrary size, we first divide it up into -byte chunks ; in other words:
We now identify each with a field element in the natural way.
and now we define
For details on how have been generated, please see Global Constants.
We use the Blake2s Hash more sparingly, because it is not SNARK-friendly, but it does exhibit psuedorandomness not offered by Pedersen. That is, it is considered a reasonable hueristic to use it in place of a random oracle used for a security proof.
We employ the standard implementation of the Blake2s hash, which is fully documented here.
The Blake2s hash is utilized for computing nullifiers and for generating pseudorandom challenges, when verifying Schnorr signatures and when recursively verifying Plonk proofs.
Global network constants
NUM_ASSETS_BIT_LENGTH
= 2NUM_ASSETS
= 1DATA_TREE_DEPTH
= 32NULL_TREE_DEPTH
= 256ROOT_TREE_DEPTH
= 28MAX_TXS_BIT_LENGTH
= 10NOTE_VALUE_BIT_LENGTH
= 252TX_FEE_BIT_LENGTH
= 254 - MAX_TXS_BIT_LENGTH
Pedersen Hash 'h' Elements
There are additionally elliptic curve group points used in the computation of Pedersen hashes.
For example:
The generator algorithm for computing the in pseudocode is:
A Note is encoded as follows:
note.nonce
note.asset_id
note.val
note.secret
note.owner.x
note.owner.y
Note: The nonce plays a role in enabling the revocation of spending keys. The secret is to construct a hiding Pedersen commitment to hide the note details.
Each is a field element in from the BN254 spec. So a note is an element of .
A Note Commitment is a Pedersen Commitment:
An Account Note associates a spending key with an account. It consists of the following three field elements.
account_alias_id
- a concatentation of the 224 bit alias_hash
, with the 32-bit nonce
. account_alias_id
is enforced to be smaller than (the bn-254 curve size), thus not all 32 byte values are possible.account_public_key.x
: the x-coordinate of the account public keyspending_public_key.x
: the x-coordinate of the spending key that is been assigned to this account via this note.The commitment to an account note , denoted , is a pedersen commitment of :
(The start from 20 is according to the constant ACCOUNT_NOTE_HASH_INDEX
)
The Account Nullifier of an account note is a pedersen hash of
A.account_alias_id
Details on this are found here
Aztec 2.0's state is recorded via three Merkle trees:
The five circuits in Aztec 2.0 are:
The Inner Circuits: 1, 2 & 3
Validated by ZK Proofs over "Outer Circuits"
The Outer Circuits: 4 & 5
Validated by Mainnet Verifier
This circuit allows notes to be spent.
The circuit takes in two input notes, and two new output notes, and updates the Note Tree and Nullifier Tree accordingly.
The inputs for the join-split circuit are:
Where the field is from the BN254 specification.
proof_id
public_input
public_output
public_asset_id
output_nc_1_x
(nc is short for note commitment)output_nc_1_y
output_nc_2_x
output_nc_2_y
nullifier_1
nullifier_2
input_owner
output_owner
data_tree_root
input_note_1.val
input_note_1.secret
input_note_1.account_id
input_note_1.asset_id
input_note_2.val
input_note_2.secret
input_note_2.account_id
input_note_2.asset_id
index_1
index_2
input_note_2.asset_id
output_note_1.val
output_note_1.secret
output_note_1.account_id
output_note_1.asset_id
output_note_1.nonce
output_note_2.val
output_note_2.secret
output_note_2.account_id
output_note_2.asset_id
output_note_2.nonce
account_note.account_id
account_note.npk
(npk=nullifier public key)account_note.spk
(spk=spending public key)index_ac
note_num
nk
(nullifier private key)signature
In the Pseudocode to follow, we use the following function names:
NC
Note commitment function, which is assumed to be
CompressNC
Note Commitment Compressor takes a note commitment (an elliptic curve point) and compresses by just taking the x coordinateNF
Nullifier Function, which we assume can be modeled as a random oracle, and only depends on AC
Account Note Commitment, which is assumed to be collision resistantUpdate
Merkle Update Function inserts a set of compressed note commitments into the note tree and validates the correctness of the associated merkle root updateThe Account Circuit enables the transfer of keys that control notes.
Unlike the Rollup Circuit, which always adds nullifiers to the tree, the Account Circuit conditionally adds nullifiers to the tree.
This condition is emulated via the production of Gibberish Nullifiers where we do not wish to add a nullifier to the nullifier set. In doing so, we must ensure that:
We achieve outcome 1. by including the proof_id
in each nullifier computation.
Ensuring outcome 2. is harder. The circuit must have a flag variable, is_nullifier_fake
, that we use to modify the input data being hashed.
account_key
)alias
at account_value_id = (account_public_key, 0)
account_id = (alice, 0)
is nullified, and new spending keys are associated with (alice, 1)
(account_public_key, 0)
, to (account_public_key, 1)
(alice, 1)
account_id = (alice, 1)
, associating new spending keys with (alice, 2)
(account_public_key, 1)
, to (account_public_key, 2)
The inputs for the account circuit are:
As previously, the field is from the BN254 specification.
Recall that all inner circuits must have the same number of public inputs as they will be used homogenously by the rollup circuit.
However, we repurpose and rename some inputs for to describe the inner circuit. We denote the renaming of a given input with the notation [old name] --> [new name]
proof_id
public_input --> acccount_pubkey_x
public_output --> account_pubkey_y
public_asset_id --> account_id
output_nc_1_x
(nc is short for note commitment)output_nc_1_y
output_nc_2_x
output_nc_2_y
nullifier_1
nullifier_2
input_owner
output_owner
data_tree_root
input_note_1.val
input_note_1.secret
input_note_1.account_id
input_note_1.asset_id
input_note_2.val
input_note_2.secret
input_note_2.account_id
index_1
index_2
input_note_2.asset_id
output_note_1.val
output_note_1.secret
output_note_1.account_id
output_note_1.asset_id
output_note_2.val
output_note_2.secret
output_note_2.account_id
output_note_2.asset_id
account_note.account_id
account_note.npk
(npk=nullifier public key)account_note.spk
(spk=spending public key)index_ac
note_num
nk
(nullifier private key)signature
None
Computed vars:
alias_hash
= account_id.slice(0, 28)
nonce
= account_id.slice(28, 4)
output_nonce
= migrate + nonce
output_account_id
= alias_hash + (output_nonce * 2^224)
assert_account_exists
= nonce != 0
signer
= nonce == 0 ? account_public_key : signing_public_key
message
= pedersen(account_public_key, account_id, spending_public_key_1.x, spending_public_key_2.x)
account_note_data
= pedersen(account_id, account_public_key.x, signer.x)
is_nullifier_fake
= migrate == 0
Computed public inputs:
output_note_1_x/y
= pedersen(output_account_id, account_public_key.x, account_public_key.y, spending_public_key_1.x, spending_public_key_1.y)
output_note_2_x/y
= pedersen(output_account_id, account_public_key.x, account_public_key.y, spending_public_key_2.x, spending_public_key_2.y)
nullifier_1
= pedersen(proof_id + (is_nullifier_fake * 2^250), account_id, !migrate * gibberish)
nullifier_2
= pedersen(proof_id + (1 * 2^250), gibberish)
Circuit constraints:
migrate == 1 || migrate == 0
verify_signature(message, signer, signature) == 1
membership_check(account_note_data, account_note_index, account_note_path, data_tree_root) == assert_account_exists
The rollup circuit aggregates proofs from a defined set of ‘inner’ circuits.
Each inner circuit has 13 public inputs. The rollup circuit will execute several defined subroutines on the public inputs.
There are public inputs, in three sections:
All are field elements. The first 11 public inputs are the following:
rollup_id
rollup_size
data_start_index
old_data_root
new_data_root
old_null_root
new_null_root
old_data_root_root
new_data_root_root
total_tx_fee
num_txs
The following inputs are private to reduce proof size:
Extract
Extraction Function extracts 14 public inputs from a proof, validates the result matches the rollup’s inner public inputsAggregate
Proof Aggregation Function for ultimate batch verification outside the circuit, given a verification key and (optional, defined by 4th input parameter) a previous output of Aggregate. Returns a BN254 point pairNonMembershipUpdate
Nullifier Update Function checks a nullifier is not in a nullifier set given its root, then inserts the nullifier and validates the correctness of the associated merkle root updateBatchUpdate
Batch Update Function inserts a set of compressed note commitments into the note tree and validates the corretness of the associated merkle root updateQ_0 = [0, 0]
num_inputs == N
i = 1, ..., num_inputs
pub_inputs = Extract(PI_i)
vk = vks[proof_id_i]
Q_i = Aggregate(PI_i, pub_inputs, vk, Q_{i-1}, (i > 1))
CompressNC(output_nc_1_x_i, output_nc_1_y_i)
CompressNC(output_nc_2_x_i, output_nc_2_y_i)
NonMembershipUpdate(
, , nullifier_1_i)
NonMembershipUpdate(
, , nullifier_2_i)
Membership(old_data_roots_root, data_tree_root_index_i, data_tree_root_i)
[P1, P2] = Q_{num_inputs}
BatchUpdate(old_data_root, new_data_root, data_start_index, leaf_1, ..., leaf_{2 * num_inputs})
old_null_root = null_root_1
new_null_root = null_root_{2 * num_inputs + 1}
This is an outer circuit, allowing the user to withdraw funds directly from the network without requiring a relayer service to roll up the transaction. It is a temporary safeguard until the node service is decentralised.
The escape hatch circuit consists of a JoinSplit circuit, combined with checks usually done in the rollup circuit. Namely, that the nullifiers and data tree root are valid.
The rollup smart contract will only accept escape hatch proofs for a two-hour window every twenty-four hours, to prevent race conditions between rollup proofs and escape hatch proofs.
A set of public inputs joinsplit_public
to the Joinsplit circuit. Including in particular ``
proof_id
public_input
public_output
public_asset_id
output_nc_1_x
(nc is short for note commitment)output_nc_1_y
output_nc_2_x
output_nc_2_y
nullifier_1
nullifier_2
input_owner
output_owner
data_tree_root
The following additional public inputs
rollup_id,data_start_index,old_data_root, new_data_root, old_null_root, new_null_root,old_data_roots_root,new_data_roots_root
joinsplit_private
of private inputs to the joinsplit circuit.None
joinsplit_public,joinsplit_private
leaf_1 = CompressNC(output_nc_1_x, output_nc_1_y)
leaf_2 = CompressNC(output_nc_2_x, output_nc_2_y)
NonMembershipUpdate(old_null_root, new_null_root, {nullifier_1,nullifier_2})
leaf_1,leaf_2
are in old_data_root
, and their addition results in new_data_root
Membership(old_data_roots_root, new_data_roots_root,data_tree_root, rollup_id)
BatchUpdate(old_data_root, new_data_root, data_start_index
, $\text{leaf}_{1}, ..., \text{leaf}_{2 * \text{num_inputs}}$)
This circuit rolls up other rollup proofs.
It is defined by a parameter rollup_num
, of inner rollups. Let's also denote rollup_num
for convenience.
The inputs for the root rollup circuit are:
As previously, the field is from the BN254 specification.
rollup_id
(The location where new_root_M
will be inserted in the roots tree)rollup_size
data_start_index
old_data_root
new_data_root
old_null_root
new_null_root
old_root_root
new_root_root
new_data_root
=old_data_root
.Update(old_data_roots_root, new_data_roots_root, rollup_id, new_data_root_M)
where is the verification key of the rollup circuit.