Counter Contract
In this guide, we will create our first Aztec.nr smart contract. We will build a simple private counter, where you can keep your own private counter - so no one knows what ID you are at or when you increment! This contract will get you started with the basic setup and syntax of Aztec.nr, but doesn't showcase all of the awesome stuff Aztec is capable of.
This tutorial is compatible with the Aztec version v2.0.2
. Install the correct version with aztec-up -v 2.0.2
. Or if you'd like to use a different version, you can find the relevant tutorial by clicking the version dropdown at the top of the page.
Prerequisites
- You have followed the quickstart
- Running Aztec Sandbox
- Installed Noir LSP (optional)
Set up a project
Run this to create a new contract project:
aztec-nargo new --contract counter
Your structure should look like this:
.
|-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="v2.0.2", directory="noir-projects/aztec-nr/aztec" }
value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="v2.0.2", directory="noir-projects/aztec-nr/value-note"}
easy_private_state = { git="https://github.com/AztecProtocol/aztec-packages/", tag="v2.0.2", directory="noir-projects/aztec-nr/easy-private-state"}
Define the functions
Go to main.nr
, and replace the boilerplate code with this contract initialization:
use dep::aztec::macros::aztec;
#[aztec]
pub contract Counter {
}
This defines a contract called Counter
.
Imports
We need to define some imports.
Write this inside your contract, ie inside these brackets:
pub contract Counter {
// imports go here!
}
use aztec::{
macros::{functions::{initializer, private, utility}, storage::storage},
oracle::debug_log::debug_log_format,
protocol_types::{address::AztecAddress, traits::ToField},
state_vars::Map,
};
use easy_private_state::EasyPrivateUint;
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L7-L15
-
use aztec::macros::{functions::{initializer, private, utility}, storage::storage},
Imports the macros needed to define function types (initializer
,private
, andutility
) and thestorage
macro for declaring contract storage structures. -
protocol_types::{address::AztecAddress, traits::ToField},
Brings inAztecAddress
(used to identify accounts/contracts) and traits for converting values to and from field elements, necessary for serialization and formatting inside Aztec. -
state_vars::Map,
Brings inMap
, used for creating state mappings, like our counters. -
use easy_private_state::EasyPrivateUint;
Imports a wrapper to manage private integer-like state variables (ie our counter), abstracting away notes.
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/test/counter_contract/src/main.nr#L17-L22
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 count 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) {
let counters = storage.counters;
counters.at(owner).add(headstart, owner);
}
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L24-L32
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) {
debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
let counters = storage.counters;
counters.at(owner).add(1, owner);
}
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L34-L44
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.
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:
#[utility]
unconstrained fn get_counter(owner: AztecAddress) -> Field {
storage.counters.at(owner).get_value()
}
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L75-L80
This is a utility
function which is used to obtain the counter information outside of 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.
Compile the smart contract
In ./counter/
directory, run these commands:
aztec-nargo compile # generate contract artifacts
aztec-postprocess-contract # transpile contracts and generate verification keys
The first command compiles your Noir contract and creates a target
folder with a .json
artifact inside. The second command processes these artifacts for use with Aztec (transpiling for the AVM and generating verification keys). 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 and processing, 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!