Skip to main content

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

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:

imports
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]
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:

constructor
#[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.

increment
#[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:

get_counter
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.

Optional: Learn more about concepts mentioned here