Counter Contract
In this guide, we will create our first Aztec.nr smart contract. We will build a simple private counter. This contract will get you started with the basic setup and syntax of Aztec.nr, but doesn't showcase the awesome stuff Aztec is capable of.
If you already have some experience with Noir and want to build a cooler contract that utilizes both private and public state, you might want to check out the token contract tutorial instead.
Prerequisites
- You have followed the quickstart
- Running Aztec Sandbox
Set up a project
Create a new directory called aztec-private-counter
mkdir aztec-private-counter
then create a contracts
folder inside where our Aztec.nr contract will live:
cd aztec-private-counter
mkdir contracts
Inside contracts
create a new project called counter
:
cd contracts
aztec-nargo new --contract counter
Your structure should look like this:
.
|-aztec-private-counter
| |-contracts
| | |--counter
| | | |--src
| | | | |--main.nr
| | | |--Nargo.toml
The file main.nr
will soon turn into our smart contract!
Add the following dependencies to Nargo.toml
:
[package]
name = "counter"
type = "contract"
authors = [""]
compiler_version = ">=0.28.0"
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.44.0", directory="noir-projects/aztec-nr/aztec" }
value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.44.0", directory="noir-projects/aztec-nr/value-note"}
easy_private_state = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.44.0", directory="noir-projects/aztec-nr/easy-private-state"}
Define the functions
Go to main.nr
and start with this contract initialization:
contract Counter {
}
This defines a contract called Counter
.
Imports
We need to define some imports.
Write this within your contract at the top
use dep::aztec::prelude::{AztecAddress, Map};
use dep::value_note::{balance_utils, value_note::{ValueNote, VALUE_NOTE_LEN}};
use dep::easy_private_state::EasyPrivateUint;
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L2-L6
context::{PrivateContext, Context}
Context gives us access to the environment information such as msg.sender
. We are also importing PrivateContext
to access necessary information for our private functions. We’ll be using it in the next step.
map::Map
Map is a private state variable that functions like a dictionary, relating Fields to other state variables.
value_note
Notes are fundamental to how Aztec manages privacy. A note is a privacy-preserving representation of an amount of tokens associated with a nullifier key (that can be owned by an owner), while encrypting the amount. In this contract, we are using the value_note
library. This is a type of note interface for storing a single Field, eg a balance - or, in our case, a counter.
We are also using balance_utils
from this import, a useful library that allows us to utilize value notes as if they are simple balances.
EasyPrivateUint
This allows us to store our counter in a way that acts as an integer, abstracting the note logic.
Declare storage
Add this below the imports. It declares the storage variables for our contract. We are going to store a mapping of values for each AztecAddress
.
#[aztec(storage)]
struct Storage {
counters: Map<AztecAddress, EasyPrivateUint>,
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L8-L13
Keep the counter private
Now we’ve got a mechanism for storing our private state, we can start using it to ensure the privacy of balances.
Let’s create a constructor method to run on deployment that assigns an initial supply of tokens to a specified owner. This function is called initialize
, but behaves like a constructor. It is the #[aztec(initializer)]
decorator that specifies that this function behaves like a constructor. Write this:
#[aztec(private)]
#[aztec(initializer)]
// We can name our initializer anything we want as long as it's marked as aztec(initializer)
fn initialize(headstart: u64, owner: AztecAddress, outgoing_viewer: AztecAddress) {
let counters = storage.counters;
counters.at(owner).add(headstart, owner, outgoing_viewer);
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L15-L23
This function accesses the counts from storage. Then it assigns the passed initial counter to the owner
's counter privately using at().add()
.
We have annotated this and other functions with #[aztec(private)]
which are ABI macros so the compiler understands it will handle private inputs. Learn more about functions and annotations here.
Incrementing our counter
Now let’s implement the increment
function we defined in the first step.
#[aztec(private)]
fn increment(owner: AztecAddress, outgoing_viewer: AztecAddress) {
dep::aztec::oracle::debug_log::debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
let counters = storage.counters;
counters.at(owner).add(1, owner, outgoing_viewer);
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L25-L32
The increment
function works very similarly to the constructor
, but instead directly adds 1 to the counter rather than passing in an initial count parameter.
Prevent double spending
Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its nullifier key. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called compute_note_hash_and_optionally_a_nullifier
to determine these values for any given note produced by this contract.
Getting a counter
The last thing we need to implement is the function in order to retrieve a counter. In the getCounter
we defined in the first step, write this:
unconstrained fn get_counter(owner: AztecAddress) -> pub Field {
let counters = storage.counters;
balance_utils::get_balance(counters.at(owner).set)
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L34-L39
This function is unconstrained
which allows us to fetch data from storage without a transaction. We retrieve a reference to the owner
's counter
from the counters
Map. The get_balance
function then operates on the owner's counter. This yields a private counter that only the private key owner can decrypt.
Compile
Now we've written a simple Aztec.nr smart contract, we can compile it with aztec-nargo
.
Compile the smart contract
In ./contracts/counter/
directory, run this:
aztec-nargo compile
This will compile the smart contract and create a target
folder with a .json
artifact inside.
After compiling, you can generate a typescript class using aztec-builder
's codegen
command. (See aztec-builder help codegen
for syntax).
In the same directory, run this:
aztec-builder codegen -o src/artifacts target
You can now use the artifact and/or the TS class in your Aztec.js!