Declaring Contract Storage
This guide shows you how to declare storage and use various storage types provided by Aztec.nr for managing contract state.
Prerequisites
- An Aztec contract project set up with
aztec-nrdependency - Understanding of Aztec's private and public state model
- Familiarity with Noir struct syntax
- Basic knowledge of maps and data structures
For storage concepts, see storage overview.
Define your storage struct
Create a storage struct with #[storage]
Declare storage using a struct annotated with #[storage]. For example:
#[storage]
struct Storage<Context> {
// The admin of the contract
admin: PublicMutable<AztecAddress, Context>,
}
Context parameter
The Context parameter provides execution mode information.
Access storage in functions
Use the storage keyword to access your storage variables in contract functions.
Use maps for key-value storage
Maps store key-value pairs where keys are Field elements and values can be any type.
You can import Map as:
use dep::aztec::state_vars::Map;
Understand map structure
- Keys: Always
Fieldor serializable types - Values: Any type, including other maps
- Multiple maps: Supported in the same contract
Declare private maps
Specify the note type for private storage maps:
private_items: Map<AztecAddress, PrivateSet<MyNote, Context>, Context>,
Declare public maps
Use PublicState for public storage maps:
authorized_users: Map<AztecAddress, PublicMutable<bool, Context>, Context>,
Access map values
Use the .at() method to access values by key:
assert(storage.authorized_users.at(context.msg_sender()).read(), "caller is not authorized");
This is equivalent to Solidity's authorized_users[msg.sender] pattern.
Use private storage types
Aztec.nr provides three private state variable types:
PrivateMutable<NoteType>: Single mutable private valuePrivateImmutable<NoteType>: Single immutable private valuePrivateSet<NoteType>: Collection of private notes
All private storage operates on note types rather than arbitrary data types. Learn how to implement custom notes and use them with Maps here
PrivateMutable
PrivateMutable is a private state variable that is unique in a way. When a PrivateMutable is initialized, a note is created to represent its value. Updating the value means to destroy the current note, and to create a new one with the updated value.
Like for public state, we define the struct to have context and a storage slot. You can view the implementation here.
An example of PrivateMutable usage in contracts is keeping track of important values. The PrivateMutable is added to the Storage struct as follows:
// #[storage]
// ...etc
my_value: PrivateMutable<MyNote, Context>,
initialize
As mentioned, the PrivateMutable should be initialized to create the first note and value. When this function is called, a nullifier of the storage slot is created, preventing this PrivateMutable from being initialized again.
Unlike public states, which have a default initial value of 0 (or many zeros, in the case of a struct, array or map), a private state (of type PrivateMutable, PrivateImmutable or PrivateSet) does not have a default initial value. The initialize method (or insert, in the case of a PrivateSet) must be called.
is_initialized
An unconstrained method to check whether the PrivateMutable has been initialized or not. It takes an optional owner and returns a boolean. You can view the implementation here (GitHub link).
let is_initialized = my_value.is_initialized();
replace
To update the value of a PrivateMutable, we can use the replace method. The method takes a function (or closure) that transforms the current note into a new one.
When called, the method will:
- Nullify the old note
- Apply the transform function to produce a new note
- Insert the new note into the data tree
An example of this is seen in an example card game, where an update function is passed in to transform the current note into a new one (in this example, updating a CardNote data):
let new_note = MyNote::new(new_value, owner);
storage.my_value.replace(&mut new_note).emit(encode_and_encrypt_note(&mut context, owner));
Calling emit(encode_and_encrypt_note()) on the replace method will encrypt the new note and post it to the data availability layer so that the note information is retrievable by the recipient.
If two people are trying to modify the PrivateMutable at the same time, only one will succeed as we don't allow duplicate nullifiers! Developers should put in place appropriate access controls to avoid race conditions (unless a race is intended!).
get_note
This function allows us to get the note of a PrivateMutable, essentially reading the value.
let note = my_value.get_note()
To ensure that a user's private execution always uses the latest value of a PrivateMutable, the get_note function will nullify the note that it is reading. This means that if two people are trying to use this function with the same note, only one will succeed (no duplicate nullifiers allowed).
This also makes read operations indistinguishable from write operations and allows the sequencer to verifying correct execution without learning anything about the value of the note.
view_note
Functionally similar to get_note, but executed in unconstrained functions and can be used by the wallet to fetch notes for use by front-ends etc.