How Aztec works?

A walkthrough of the circuits in Aztec 2.0

Account Circuit

  • A user creates an account on zk.money using an alias and a nonce \(n \in \mathbb{Z}^{32}_2\)
  • We compute an account identifier as: 

\(a_{\text{id}} \coloneqq \left( n \ \| \ H_{B}\left(\texttt{suyashbagad}\right)[ \ 0 : 224 \ ]\right) \in \mathbb{Z}^{256}_2\)

  • Account information is stored in account notes

Account PK

Account id

Spending PK1

\(a_{\text{id}} \ \in \ \mathbb{Z}_2^{32}\)

\(S_1 \ \in \ \mathbb{G}_1\)

\(A \ \in \ \mathbb{G}_1\)

Account PK

Account id

Spending PK2

\(a_{\text{id}} \ \in \ \mathbb{Z}_2^{32}\)

\(S_2 \ \in \ \mathbb{G}\)

\(A \ \in \ \mathbb{G}_1\)

  • Spending keys are used for signing transactions
  • Nonce \(n\) is used as an identifier of the account PK of an account
  • Allows account migration, i.e. updating the account PK
  • Spending keys can be updated without necessarily migrating

Account Circuit

  • Let's look at an example of account migration
  • New account notes must have same alias but different nonce
  • Need to track the old and new account notes!
    • Check if the old notes actually exist in the data tree: \(\mathfrak{C}(\mathcal{A}_{1, \text{old}}), \ \mathfrak{C}(\mathcal{A}_{2, \text{old}}) \in \mathbb{D}\)
    • Add old note nullifiers to the nullifier tree:  \( \mathbb{N} \longleftarrow \mathfrak{N}(a_{\text{id, old}})\)
    • Add new note commitments to the data tree: \( \mathbb{D} \longleftarrow \left\{ \mathfrak{C}(\mathcal{A}_{1, \text{new}}), \ \mathfrak{C}(\mathcal{A}_{2, \text{new}}) \right\}\)
  • Check the validity of signature \(\sigma(a_{\text{id, old}}, A_{\text{old}}, A_{\text{new}}, S_{1, \text{old}}, S_{2, \text{old}})\)
  • If not migrating, set \(n_{\text{new}} = n_{\text{old}}\) and check \(A_{\text{old}} = A_{\text{new}}\)

\(a_{\text{id, new}}\)

\(S_{1, {\text{new}}} \)

\(A_{\text{new}} \)

\(a_{\text{id, old}}\)

\(S_{2, {\text{new}}} \)

\(A_{\text{new}} \)

\(\mathcal{A}_{2, \text{new}}\)

\(\mathcal{A}_{1, \text{new}}\)

\(a_{\text{id, old}}\)

\(S_{1, {\text{old}}} \)

\(A_{\text{old}} \)

\(\mathcal{A}_{1, \text{old}}\)

\(a_{\text{id, old}}\)

\(S_{2, {\text{old}}} \)

\(A_{\text{old}} \)

\(\mathcal{A}_{2, \text{old}}\)

Value Notes

  • Aztec uses notes as a basis for private transactions on Ethereum

Value

Asset id

Nonce

Owner

Secret

\(a \ \in \ \mathbb{Z}_2^{32}\)

\(A \ \in \ \mathbb{G}_1\)

\(n \ \in \ \mathbb{Z}_2^{32}\)

\(v \ \in \ \mathbb{F}_q\)

\(s \ \in \ \mathbb{F}_q\)

  • A value note is given as: \(\mathcal{V} = \{a, v, n, \mathcal{O}, s\}\)
  • The nonce here is same as the one used in an account note
  • A note incorporates the on-chain identity (i.e. account PK) of its owner
  • The secret \(s\) is the hiding factor in computing Pedersen commitment to a note:
\mathfrak{C}(\mathcal{V}) \coloneqq aG_0 + vG_1 + nG_2 + A_xG_3 + A_yG_4 + sG_5

Join-Split Circuit

  • A join-split transaction spends two input notes and creates two new output notes 

\(\mathcal{V}^{\text{in}}_{1} = \{a^{\text{in}}_1, v^{\text{in}}_1, n^{\text{in}}_1, A^{\text{in}}_1, s^{\text{in}}_1 \}\)

\(\mathcal{V}^{\text{in}}_{2} = \{a^{\text{in}}_2, v^{\text{in}}_2, n^{\text{in}}_2, A^{\text{in}}_2, s^{\text{in}}_2 \}\)

\(\mathcal{V}^{\text{out}}_{1} = \{a^{\text{out}}_1, v^{\text{out}}_1, n^{\text{out}}_1, A^{\text{out}}_1, s^{\text{out}}_1 \}\)

\(\mathcal{V}^{\text{out}}_{2} = \{a^{\text{out}}_2, v^{\text{out}}_2, n^{\text{out}}_2, A^{\text{out}}_2, s^{\text{out}}_2 \}\)

  • A join-split transaction must obey the following:
    • Asset ids must match: \(a_1^{\text{in}} = a_2^{\text{in}} = a_1^{\text{out}} = a_2^{\text{out}} =: a\)
    • Input note owners must match: \(A_{1}^{\text{in}} = A_{2}^{\text{in}}\)
    • Input note nonces match: \(n^{\text{in}}_1 = n^{\text{in}}_2 = n\)
    • Amounts are balanced: \(v^{\text{in}}_1 + v^{\text{in}}_2 = v^{\text{out}}_1 + v^{\text{out}}_2 +  \text{tx\_fee}\)
    • Range constraints: \(\text{tx\_fee} \le 2^{242}, a \le 2^{32}\)

Join-Split Circuit

  • A join-split tx also supports publicly visible deposit and withdraw amounts 
  • The amount balance check changes to \(v^{\text{in}}_1 + v^{\text{in}}_2 + v_{\text{PI}}  = v^{\text{out}}_1 + v^{\text{out}}_2 + \text{tx\_fee} + v_{\text{PO}}\)
  • Range constraints on public input and outputs: \(v_{\text{PI}}, v_{\text{PO}} \le 2^{252}\)
  • To track the input and output notes across multiple join-split txs, we need to ensure
    • Input notes must be present in the data tree:  \( \mathfrak{C}(\mathcal{V}_{1}^{\text{in}}), \mathfrak{C}(\mathcal{V}_{2}^{\text{in}}) \in \mathbb{D}\)
    • Input notes are not already spent:  \( \mathfrak{N}(\mathcal{V}_{1}^{\text{in}}), \mathfrak{N}(\mathcal{V}_{2}^{\text{in}}) \notin \mathbb{N}\)
    • Nullify the input notes to avoid double-spending:   \(\mathbb{N} \longleftarrow \mathfrak{N}(\mathcal{V}_{1}^{\text{in}}), \mathfrak{N}(\mathcal{V}_{2}^{\text{in}})\)
    • Output notes are added to the data tree:  \(\mathbb{D} \longleftarrow \mathfrak{C}(\mathcal{V}_{1}^{\text{out}}), \mathfrak{C}(\mathcal{V}_{2}^{\text{out}})\)

\(v_{\text{PI}}\)

\(v_{\text{PO}}\)

\(\mathcal{V}^{\text{in}}_{1}\)

\(\mathcal{V}^{\text{out}}_{1}\)

\(\mathcal{V}^{\text{out}}_{2}\)

\(\mathcal{V}^{\text{in}}_{2}\)

Merkle Trees

  • Note commitments are stored in \(\mathbb{D}\) while spent note nullifiers in \(\mathbb{N}\)
  • Suppose we want to store \(2^{k}\) files in a decentralized and succinct way

\(f_1\)

\(f_2\)

\(f_3\)

\(f_4\)

\(f_5\)

\(f_6\)

\(f_7\)

\(f_8\)

\(H(f_1)\)

\(H(f_2)\)

\(H(f_3)\)

\(H(f_4)\)

\(H(f_5)\)

\(H(f_6)\)

\(H(f_7)\)

\(H(f_8)\)

Merkle Trees

  • Note commitments are stored in \(\mathbb{D}\) while spent note nullifiers in \(\mathbb{N}\)
  • Suppose we want to store \(2^{k}\) files in a decentralized and succinct way

\(H(f_1)\)

\(H(f_2)\)

\(H(f_3)\)

\(H(f_4)\)

\(H(f_5)\)

\(H(f_6)\)

\(H(f_7)\)

\(H(f_8)\)

\(H'(H(f_1), H(f_2))\)

\(H'(H(f_3), H(f_4))\)

\(H'(H(f_5), H(f_6))\)

\(H'(H(f_7), H(f_8))\)

\(h^1_1\)

\(h^1_2\)

\(h^1_3\)

\(h^1_4\)

\(h^2_1\)

\(h^2_2\)

\(h^3_1\)

\(H'(h^1_1, h^1_2)\)

\(H'(h^1_3, h^1_4)\)

\(H'(h^2_1, h^2_2)\)

Merkle Trees

\(H(f_1)\)

\(H(f_2)\)

\(H(f_3)\)

\(H(f_4)\)

\(H(f_5)\)

\(H(f_6)\)

\(H(f_7)\)

\(H(f_8)\)

\(h^1_1\)

\(h^1_2\)

\(h^1_3\)

\(h^1_4\)

\(h^2_1\)

\(h^2_2\)

\(h^3_1\)

  • Indeed, \(h_1^3\) is succinct form of the files. How do we prove inclusion?

Merkle Trees

  • Indeed, \(h_1^3\) is succinct form of the files. How do we prove inclusion?
  • Only \(\left( H(f_6), h^1_4, h_1^2 \right)\) are enough to prove inclusion of \(f_5\)! Sister nodes!

\(H(f_1)\)

\(H(f_2)\)

\(H(f_3)\)

\(H(f_4)\)

\(H(f_5)\)

\(H(f_6)\)

\(H(f_7)\)

\(H(f_8)\)

\(h^1_1\)

\(h^1_2\)

\(h^1_3\)

\(h^1_4\)

\(h^2_1\)

\(h^2_2\)

\(h^3_1\)

Merkle Trees

  • For root \(h_1^{3}\), index \(\text{idx} = 5\), the Merkle proof is \(\left\{H(f_6), h_4^1, h_1^2\right\}\).

\(H(f_1)\)

\(H(f_2)\)

\(H(f_3)\)

\(H(f_4)\)

\(H(f_5)\)

\(H(f_6)\)

\(H(f_7)\)

\(H(f_8)\)

\(h^1_1\)

\(h^1_2\)

\(h^1_3\)

\(h^1_4\)

\(h^2_1\)

\(h^2_2\)

\(h^3_1\)

Data Tree

  • \(\mathbb{D}\) is size \(2^{32}\) Merkle tree which supports batch updates
  • Contains commitments to all account and value notes ever created in Aztec  
  • Suppose we wish to add \(\mathcal{A}_1, \mathcal{A}_2, \mathcal{V}_1, \mathcal{V}_2\) to \(\mathbb{D}\)

Data Tree

  • Old data root: \(D_{\text{old}},\)

\(\mathfrak{C}(\mathcal{A}_1)\)

\(\mathfrak{C}(\mathcal{A}_2)\)

\(\mathfrak{C}(\mathcal{V}_1)\)

\(\mathfrak{C}(\mathcal{V}_2)\)

\(D\)

New data root: \(D_{\text{new}}\)

Data Tree

  • Old data root: \(D_{\text{old}},\)
  • With subtree root \(S\) and the partial proof \(\{h_1, h_2\}\), we can verify:

\(\mathfrak{C}(\mathcal{A}_1)\)

\(\mathfrak{C}(\mathcal{A}_2)\)

\(\mathfrak{C}(\mathcal{V}_1)\)

\(\mathfrak{C}(\mathcal{V}_2)\)

\(D\)

New data root: \(D_{\text{new}}\)

\(S\)

\(h_1\)

\(h_2\)

\(D_{\text{new}} \stackrel{?}{=} H\left(h_2, H(S, h_1)\right)\)

Nullifier Tree

  • \(\mathbb{N}\) is size \(2^{256}\) sparse Merkle tree which supports non-membership proofs
  • To prove that a note is unspent, we need to give a non-membership proof
  • Suppose we have \(16\) leaf values \(\{A, B, \dots, P\}\) s.t. \(\text{idx}(A) = 1\) and so on
  • To prove \(J \notin \mathbb{T}\), a membership proof \((10, \phi, \pi_{\text{merkle}})\) suffices!

\(A\)

\(F\)

\(N\)

\(\phi\)

Nullifier Tree

  • \(\mathbb{N}\) is size \(2^{256}\) sparse Merkle tree which supports non-membership proofs
  • To prove that a note is unspent, we need to give a non-membership proof
  • A nullifier of a note is a computed as

\(A\)

\(F\)

\(N\)

\(\phi\)

\(\mathfrak{N}(\mathcal{V}) = H_B\left(H_P\left( \ \mathfrak{C}(\mathcal{V})_x, \ \text{idx}, \ \text{nk} \ \right)\right) \in \mathbb{F}_q, \ q \approx 2^{254}\)

Up Next

  • In the next part, we will cover:
    • How multiple plonk proofs are aggregated for faster verification
    • How the rollup and root rollup circuits work
    • How does a rollup provider perform state updates
    • How does an escape hatch circuit work (brief overview)
  • Read more about our merkle trees here 

Aztec Circuits I

By Suyash Bagad

Aztec Circuits I

A very short presentation explaining how circuits in Aztec are structured.

  • 106