Storage
Smart contracts rely on storage, acting as the persistent memory on the blockchain. In Aztec, because of its hybrid, privacy-first architecture, the management of this storage is more complex than other blockchains like Ethereum.
To learn how to define a storage struct, read this guide. To learn more about storage slots, read this explainer.
You control this storage in Aztec using a struct annotated with #[aztec(storage)]
. This struct serves as the housing unit for all your smart contract's state variables - the data it needs to keep track of and maintain.
These state variables come in two forms: public and private. Public variables are visible to anyone, and private variables remain hidden within the contract. A state variable with both public and private components is said to be shared.
Aztec.nr has a few abstractions to help define the type of data your contract holds. These include PrivateMutable, PrivateImmutable, PublicMutable, PrivateSet, and SharedImmutable.
On this and the following pages in this section, you’ll learn:
- How to manage a smart contract's storage structure
- The distinctions and applications of public and private state variables
- How to use PrivateMutable, PrivateImmutable, PrivateSet, PublicMutable, SharedImmutable and Map
- An overview of 'notes' and the UTXO model
- Practical implications of Storage in real smart contracts In an Aztec.nr contract, storage is to be defined as a single struct, that contains both public and private state variables.
The Context
parameter
Aztec contracts have three different modes of execution: private, public and top-level unconstrained. How storage is accessed depends on the execution mode: for example, PublicImmutable
can be read in all execution modes but only initialized in public, while PrivateMutable
is entirely unavailable in public.
Aztec.nr prevents developers from calling functions unavailable in the current execution mode via the context
variable that is injected into all contract functions. Its type indicates the current execution mode:
&mut PrivateContext
for private execution&mut PublicContext
for public executionUncontrainedContext
for top-level unconstrained execution
All state variables are generic over this Context
type, and expose different methods in each execution mode. In the example above, PublicImmutable
's initialize
function is only available with a public execution context, and so the following code results in a compilation error:
#[aztec(storage)]
struct Storage {
variable: PublicImmutable<Field>,
}
#[aztec(private)]
fn some_private_function() {
storage.variable.initialize(0);
// ^ ERROR: Expected type PublicImmutable<_, &mut PublicContext>, found type PublicImmutable<Field, &mut PrivateContext>
}
The Context
generic type parameter is not visible in the code above as it is automatically injected by the #[aztec(storage)]
macro, in order to reduce boilerplate. Similarly, all state variables in that struct (e.g. PublicImmutable
) similarly have that same type parameter automatically passed to them.
Map
A map
is a state variable that "maps" a key to a value. It can be used with private or public storage variables.
In Aztec.nr, keys are always Field
s, or types that can be serialized as Fields, and values can be any type - even other maps. Field
s are finite field elements, but you can think of them as integers.
It includes a Context
to specify the private or public domain, a storage_slot
to specify where in storage the map is stored, and a start_var_constructor
which tells the map how it should operate on the underlying type. This includes how to serialize and deserialize the type, as well as how commitments and nullifiers are computed for the type if it's private.
You can view the implementation in the Aztec.nr library here.
You can have multiple map
s in your contract that each have a different underlying note type, due to note type IDs. These are identifiers for each note type that are unique within a contract.
new
When declaring the storage for a map, we use the Map::new()
constructor. As seen below, this takes the storage_slot
and the start_var_constructor
along with the Context
.
We will see examples of map constructors for public and private variables in later sections.
As private storage
When declaring a mapping in private storage, we have to specify which type of Note to use. In the example below, we are specifying that we want to use the PrivateMutable
note type.
In the Storage struct:
legendary_card: PrivateMutable<CardNote>,
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/main.nr#L33-L35
Public Example
When declaring a public mapping in Storage, we have to specify that the type is public by declaring it as PublicState
instead of specifying a note type like with private storage above.
minters: Map<AztecAddress, PublicMutable<bool>>,
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L37-L39
at
When dealing with a Map, we can access the value at a given key using the ::at
method. This takes the key as an argument and returns the value at that key.
This function behaves similarly for both private and public maps. An example could be if we have a map with minters
, which is mapping addresses to a flag for whether they are allowed to mint tokens or not.
assert(storage.minters.at(context.msg_sender()).read(), "caller is not minter");
Source code: noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L168-L170
Above, we are specifying that we want to get the storage in the Map at
the msg_sender()
, read the value stored and check that msg_sender()
is indeed a minter. Doing a similar operation in Solidity code would look like:
require(minters[msg.sender], "caller is not minter");