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.
Prerequisites
- You have followed the quickstart
- Running Aztec Sandbox
- Installed Noir LSP (optional)
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
under the autogenerated content:
[dependencies]
aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.63.1", directory="noir-projects/aztec-nr/aztec" }
value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.63.1", directory="noir-projects/aztec-nr/value-note"}
easy_private_state = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.63.1", directory="noir-projects/aztec-nr/easy-private-state"}
Define the functions
Go to main.nr
and start with this contract initialization:
use dep::aztec::macros::aztec;
#[aztec]
contract Counter {
}
This defines a contract called Counter
.
Imports
We need to define some imports.
Write this within your contract at the top:
use aztec::macros::{functions::{initializer, private}, storage::storage};
use aztec::prelude::{AztecAddress, Map};
use easy_private_state::EasyPrivateUint;
use value_note::{balance_utils, value_note::ValueNote};
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L7-L12
AztecAddress, Map
AztecAddress
is a type for storing contract (including account) addresses. 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
.
#[storage]
struct Storage<Context> {
counters: Map<AztecAddress, EasyPrivateUint<Context>, Context>,
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L14-L19
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 #[initializer]
decorator that specifies that this function behaves like a constructor. Write this:
#[initializer]
#[private]
// 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, context.msg_sender());
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L21-L29
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 #[private]
which are ABI macros so the compiler understands it will handle private inputs.
Incrementing our counter
Now let’s implement the increment
function we defined in the first step.
#[private]
fn increment(owner: AztecAddress, outgoing_viewer: AztecAddress) {
unsafe {
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, outgoing_viewer);
}
Source code: noir-projects/noir-contracts/contracts/counter_contract/src/main.nr#L31-L43
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#L44-L50
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. Do not worry if you see some warnings - Aztec is in fast development and it is likely you will see some irrelevant warning messages.
After compiling, you can generate a typescript class using aztec codegen
command.
In the same directory, run this:
aztec codegen -o src/artifacts target
You can now use the artifact and/or the TS class in your Aztec.js!
Next Steps
Write a slightly more complex Aztec contract
Follow the private voting contract tutorial on the next page.