Skip to main content
Version: Next

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 master. Install the correct version with aztec-up master. 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

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="master", directory="noir-projects/aztec-nr/aztec" }
value_note = { git="https://github.com/AztecProtocol/aztec-packages/", tag="master", directory="noir-projects/aztec-nr/value-note"}
easy_private_state = { git="https://github.com/AztecProtocol/aztec-packages/", tag="master", 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!
}
imports
use aztec::macros::{functions::{initializer, private, utility}, storage::storage};
use aztec::prelude::{AztecAddress, Map};
use aztec::protocol_types::traits::{FromField, ToField};
use easy_private_state::EasyPrivateUint;
use value_note::{balance_utils, value_note::ValueNote};
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L8-L14
  • use aztec::macros::{functions::{initializer, private, utility}, storage::storage};
    Imports the macros needed to define function types (initializer, private, and utility) and the storage macro for declaring contract storage structures.

  • use aztec::prelude::{AztecAddress, Map};
    Brings in AztecAddress (used to identify accounts/contracts) and Map (used for creating state mappings, like our counters).

  • use aztec::protocol_types::traits::{FromField, ToField};
    Provides traits for converting values to and from field elements, necessary for serialization and formatting inside Aztec.

  • use easy_private_state::EasyPrivateUint;
    Imports a wrapper to manage private integer-like state variables (ie our counter), abstracting away notes.

  • use value_note::{balance_utils, value_note::ValueNote};
    Brings in ValueNote, which represents a private value stored as a note, and balance_utils, which makes working with notes feel like working with simple balances.

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/test/counter_contract/src/main.nr#L16-L21

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:

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) {
let counters = storage.counters;
counters.at(owner).add(headstart, owner, context.msg_sender());
}
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L23-L31

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, sender: 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, sender);
}
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L33-L45

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. You can learn more about nullifiers and private state in the Learn section.

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
#[utility]
unconstrained fn get_counter(owner: AztecAddress) -> Field {
let counters = storage.counters;
balance_utils::get_balance(counters.at(owner).set)
}
Source code: noir-projects/noir-contracts/contracts/test/counter_contract/src/main.nr#L85-L92

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 with aztec-nargo.

Compile the smart contract

In ./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!

Investigate the increment function

Private functions in Aztec contracts are executed client-side, to maintain privacy. Developers need to be mindful of how computationally expensive it is to generate client side proofs for the private functions in the contract they write. To help understand the cost, we can use the Aztec flamegraph tool. The tool takes a contract artifact and function and generates an SVG file that shows the constraint count of each step in the function.

Run it for the increment function:

SERVE=1 aztec flamegraph target/counter-Counter.json increment

SERVE=1 will start a local server to view the flamegraph in the browser. You can also run it without this flag and open the generated SVG file in your browser manually.

Note the total gate count at the bottom of the image. The image is interactive; you can hover over different parts of the graph to see the full function name of the execution step and its gate count. This tool also provides insight into the low-level operations that are performed in the private function. Don't worry about the details of the internals of the function right now, just be aware that the more complex the function, the more gates it will use and try out the flamegraph tool on your own functions.

Read more about profiling transactions with the flamegraph tool.

For more information about writing efficient private functions, see this page of the Noir documentation.

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