Skip to content

Commit 3fb9748

Browse files
nventuroAztecBot
authored andcommitted
docs: add map and state variable docs (#22824)
These contain a basic explainer on some state variable concepts, add some missing bits there (like a table with different kinds of statevars and their usecases), and greatly expands on (completes) `Map` documentation.
1 parent 93b5ea8 commit 3fb9748

4 files changed

Lines changed: 167 additions & 77 deletions

File tree

noir-projects/aztec-nr/aztec/src/state_vars/map.nr

Lines changed: 80 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,88 @@ use crate::state_vars::StateVariable;
33

44
/// A key-value container for state variables.
55
///
6-
/// A key-value storage container that maps keys to state variables, similar to Solidity mappings.
6+
/// A `Map` wraps another state variable type (the 'value') and creates independent instances of it for each key,
7+
/// resulting in effectively as many state variables as there are key values. This behavior is similar to a Solidity
8+
/// `mapping (K => V)`.
79
///
8-
/// `Map` enables you to associate keys (like addresses or other identifiers) with state variables in your Aztec smart
9-
/// contract. This is conceptually similar to Solidity's `mapping(K => V)` syntax, where you can store and retrieve
10-
/// values by their associated keys.
10+
/// ## Use Cases
1111
///
12-
/// You can declare a state variable contained within a Map in your contract's
13-
/// [`storage`](crate::macros::storage::storage) struct.
12+
/// Any scenario in which a fixed number of variables is insufficient to represent contract state requires `Map` e.g.
13+
/// per-election configuration in a voting contract, per-user state, etc. `Map` also composes naturally into more
14+
/// complex containers: an array could be implemented as a `Map` with indices as keys.
1415
///
15-
/// For example, you might use `Map<AztecAddress, PublicMutable<FieldNote, Context>, Context>` to track token balances
16-
/// for different users, similar to how you'd use `mapping(address => uint256)` in Solidity.
16+
/// Note that some private state variables, such as [`PrivateMutable`](crate::state_vars::PrivateMutable) and
17+
/// [`PrivateSet`](crate::state_vars::PrivateSet) cannot be wrapped in a `Map`. These types implement the
18+
/// [`OwnedStateVariable`](crate::state_vars::OwnedStateVariable) trait, and as such should instead be wrapped using the
19+
/// [`Owned`](crate::state_vars::Owned) container, which can be thought of as a specialization of `Map` for private
20+
/// state 'owned' by a single account.
1721
///
18-
/// > Aside: the verbose `Context` in the declaration is a consequence of > leveraging Noir's regular syntax for
19-
/// generics to ensure that certain > state variable methods can only be called in some contexts (private, > public,
20-
/// utility).
22+
/// ## Examples
2123
///
22-
/// The methods of Map are:
23-
/// - `at` (access state variable for a given key) (see the method's own doc comments for more info).
24+
/// Since `Map` is also a [`StateVariable`], it is declared in the contract's
25+
/// [`#[storage]`](crate::macros::storage::storage) struct along with all others:
2426
///
25-
/// ## Generic Parameters
26-
/// - `K`: The key type (must implement `ToField` trait for hashing)
27-
/// - `V`: The value type:
28-
/// - any Aztec state variable (variable that implements the StateVariable trait):
29-
/// - `PublicMutable`
30-
/// - `PublicImmutable`
31-
/// - `DelayedPublicMutable`
32-
/// - `Map`
33-
/// - `Context`: The execution context (handles private/public function contexts)
27+
/// ```noir
28+
/// #[storage]
29+
/// struct Storage<C> {
30+
/// is_admin: Map<AztecAddress, PublicMutable<bool, C>, C>,
31+
/// vote_tallies: Map<ElectionId, PublicMutable<u128, C>, C>,
32+
/// }
33+
/// ```
3434
///
35-
/// ## Usage Maps are typically declared in your contract's [`storage`](crate::macros::storage::storage) struct and accessed using the `at(key)` method to get the state variable for a specific key. The resulting state variable can then be read from or written to using its own methods.
35+
/// ### Multiple `Map`s
3636
///
37-
/// Note that maps cannot be used with owned state variables (variables that implement the OwnedStateVariable trait) -
38-
/// those need to be wrapped in an `Owned` state variable instead.
37+
/// There is no limit to how many `Map` containers a contract can have, and `Map`s can themselves wrap other `Map`s,
38+
/// resulting in nested layouts.
3939
///
40-
/// ## Advanced Internally, `Map` uses a single base storage slot to represent the mapping itself, similar to Solidity's approach. Individual key-value pairs are stored at derived storage slots computed by hashing the base storage slot with the key using Poseidon2. This ensures:
41-
/// - No storage slot collisions between different keys
42-
/// - Uniform distribution of storage slots across the storage space
43-
/// - Compatibility with Aztec's storage tree structure
44-
/// - Gas-efficient storage access patterns similar to Solidity mappings
40+
/// ```noir
41+
/// #[storage]
42+
/// struct Storage<C> {
43+
/// // A nested map where the first key is an address and the second a `Year` type, such that
44+
/// // self.storage.user_yearly_config.at(user).at(year) results in distinct `UserConfig` variables for each
45+
/// // `(user, year)` tuple.
46+
/// user_yearly_config: Map<AztecAddress, Map<Year, UserConfig<C>, C>, C>,
47+
/// }
48+
/// ```
4549
///
46-
/// The storage slot derivation uses `derive_storage_slot_in_map(base_slot, key)` which computes
47-
/// `poseidon2_hash([base_slot, key.to_field()])`, ensuring cryptographically secure slot separation.
50+
/// ## Requirements
4851
///
52+
/// The value type `V` must implement the [`StateVariable`] trait. The key type `K` must implement the [`ToField`] trait
53+
/// in such a way that the `Field` representation is unique for each key.
54+
///
55+
/// For key types that cannot be converted into a `Field`, consider wrapping them in a type that implements `ToField` as
56+
/// the hash of the key.
57+
///
58+
/// ```noir
59+
/// struct MyKey {
60+
/// a: Field,
61+
/// b: Field,
62+
/// }
63+
///
64+
/// struct MyKeyWrapper {
65+
/// inner: MyKey,
66+
/// }
67+
///
68+
/// impl ToField for MyKeyWrapper {
69+
/// fn to_field(self) -> Field {
70+
/// poseidon2_hash([self.inner.a, self.inner.b])
71+
/// }
72+
/// }
73+
///
74+
/// #[storage]
75+
/// struct Storage<C> {
76+
/// a: Map<MyKeyWrapper, PublicMutable<bool, C>, C>,
77+
/// }
78+
/// ```
79+
///
80+
/// ## Implementation Details
81+
///
82+
/// Like all other state variables, `Map` gets assigned a unique storage slot. It uses this value to derive unique
83+
/// storage slots for each key, resulting in independent state variables. Nothing gets stored at `Map`'s storage slot.
84+
/// This is equivalent to Solidity's implementation of `mapping`.
85+
///
86+
/// Because the storage slot derivation is done using a hash function, it is not possible to compute the key (preimage)
87+
/// used to obtain a given derived state variable slot.
4988
pub struct Map<K, V, Context> {
5089
pub context: Context,
5190
storage_slot: Field,
@@ -66,35 +105,20 @@ impl<K, V, Context> StateVariable<1, Context> for Map<K, V, Context> {
66105
}
67106

68107
impl<K, V, Context> Map<K, V, Context> {
69-
/// Returns the state variable associated with the given key.
108+
/// Returns the state variable for a key.
70109
///
71-
/// This is equivalent to accessing `mapping[`key`]` in Solidity. It returns the state variable instance for the
72-
/// specified key, which can then be used to read or write the value at that key.
110+
/// This derives a unique storage slot for `key` and returns a state variable of type `V` at that slot. It is
111+
/// equivalent to accessing a `mapping` via the `[]` operator in Solidity (e.g. `balances[user]` would be
112+
/// `balances.at(user)`).
73113
///
74-
/// Unlike Solidity mappings which return the value directly, this returns the state variable wrapper (like
75-
/// PublicMutable, nested Map etc.) that you then call methods on to interact with the actual value.
76-
///
77-
/// # Arguments
78-
///
79-
/// * `key` - The key to look up in the map. Must implement the ToField trait (which most basic Noir & Aztec types
80-
/// do).
81-
///
82-
/// # Returns
83-
///
84-
/// * `V` - The state variable instance for this key. You can then call methods like `.read()`, `.write()`,
85-
/// `.get_note()`, etc. on this depending on the specific state variable type.
86-
///
87-
/// # Example
114+
/// ## Example
88115
///
89116
/// ```noir
90-
/// // Get a user's balance (assuming PrivateMutable<FieldNote>)
91-
/// let user_balance = self.storage.balances.at(user_address);
92-
/// let current_note = user_balance.get_note();
93-
///
94-
/// // Update the balance
95-
/// user_balance.replace(new_note);
117+
/// #[external("utility")]
118+
/// fn get_election_vote_tallies(election: ElectionId) -> u128 {
119+
/// self.storage.vote_tallies.at(election).read()
120+
/// }
96121
/// ```
97-
///
98122
pub fn at<let N: u32>(self, key: K) -> V
99123
where
100124
K: ToField,

noir-projects/aztec-nr/aztec/src/state_vars/mod.nr

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// noir-fmt:ignore
12
//! Storage for contract state.
23
//!
34
//! Contracts store their state in _state variables_. In Solidity, a state variable is simply any value declared inside
@@ -8,6 +9,81 @@
89
//! state variables, each with their nuance and use cases. Understanding these is key to understanding how a contract
910
//! works.
1011
//!
12+
//! ## Declaration
13+
//!
14+
//! All of a contract's state variables, both public and private, are declared in a single place: as members of a struct
15+
//! to which the [`#[storage]`](crate::macros::storage::storage) macro is applied.
16+
//!
17+
//! ```noir
18+
//! #[storage]
19+
//! struct Storage<C> {
20+
//! a: PublicMutable<bool, C>,
21+
//! b: Map<AztecAddress, DelayedPublicMutable<u128, C>, C>,
22+
//! c: Owned<PrivateSet<UintNote, C>, C>,
23+
//! }
24+
//! ```
25+
//!
26+
//! ## Choosing a State Variable
27+
//!
28+
//! The very first question to answer is whether the contents of the variable should be _public_ (everyone in the
29+
//! network can see their contents, like Solidity state variables), or _private_ (only some people know what is stored
30+
//! in them).
31+
//!
32+
//! ### Public State Variables
33+
//!
34+
//! Public state variables typically store their contents in the public storage tree, and can therefore only be written
35+
//! to by public contract functions.
36+
//!
37+
//! | State Variable | Mutable? | Readable in Private? | Writable in Private? | Example Use Case |
38+
//! | ----------------------------------------------------------------- | ------------------- | -------------------- | -------------------- | ---------------------------------------------------------------------------------- |
39+
//! | [`PublicMutable`](crate::state_vars::PublicMutable) | yes | no | no | Configuration of admins, global state (e.g. token total supply, total votes) |
40+
//! | [`PublicImmutable`](crate::state_vars::PublicImmutable) | no | yes | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) |
41+
//! | [`DelayedPublicMutable`](crate::state_vars::DelayedPublicMutable) | yes (after a delay) | yes | no | Non time sensitive system configuration |
42+
//!
43+
//! ### Private State Variables
44+
//!
45+
//! Private state variables typically store their contents in the note and nullifier trees, and are therefore mainly
46+
//! interacted with from private contract functions.
47+
//!
48+
//! | State Variable | Mutable? | Cost to Read? | Writable by Third Parties? | Example Use Case |
49+
//! | --------------------------------------------------------- | -------- | ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- |
50+
//! | [`PrivateMutable`](crate::state_vars::PrivateMutable) | yes | yes | no | Mutable user state only accessible by them (e.g. user settings or keys) |
51+
//! | [`PrivateImmutable`](crate::state_vars::PrivateImmutable) | no | no | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) |
52+
//! | [`PrivateSet`](crate::state_vars::PrivateSet) | yes | yes | yes | Aggregated state others can add to, e.g. token balance (set of amount notes), NFT collections (set of NFT ids) |
53+
//!
54+
//! ## Storage Slots
55+
//!
56+
//! Each state variable in a contract gets assigned a unique storage slot, which isolates the different variables so
57+
//! that reads and writes to one do not interfere with others.
58+
//!
59+
//! Storage slot allocation is automatic and cannot be overridden. A state variable's storage slot can be queried via
60+
//! [`crate::state_vars::StateVariable::get_storage_slot`].
61+
//!
62+
//! ```noir
63+
//! #[storage]
64+
//! struct Storage<C> {
65+
//! a: PublicMutable<bool, C>,
66+
//! }
67+
//!
68+
//! #[external("utility")]
69+
//! fn get_a_storage_slot() -> Field {
70+
//! self.storage.a.get_storage_slot()
71+
//! }
72+
//! ```
73+
//!
74+
//! ## The Context Parameter
75+
//!
76+
//! All state variables take a generic `Context` parameter, which will be set to one of
77+
//! [`PrivateContext`](crate::context::PrivateContext), [`PublicContext`](crate::context::PublicContext) or
78+
//! [`UtilityContext`](crate::context::UtilityContext) depending on the contract function type. This leads to some
79+
//! unfortunate boilerplate when declaring contract storage.
80+
//!
81+
//! Different methods are available depending on the context, reflecting that certain actions can only be performed in
82+
//! some or other context. For example, [`PublicMutable`](crate::state_vars::PublicMutable)'s
83+
//! [`read`](crate::state_vars::PublicMutable::read) function is available under both `PublicContext` and
84+
//! `UtilityContext`, but not `PrivateContext` since `PublicMutable` cannot be read from private functions. Similarly,
85+
//! [`write`](crate::state_vars::PublicMutable::write) is only available in `PublicContext`.
86+
//!
1187
//! ## Packing for Efficient Access
1288
//!
1389
//! Because all state variables are fully independent, when a contract reads or writes one of them all others are left
@@ -19,16 +95,15 @@
1995
//! manually implemented - see the [`Packable`](crate::protocol::traits::Packable)'s docs on how to do this.
2096
//!
2197
//! ```noir
22-
//! // Inefficient reads and writes - each bool is assigned a distinct storage slot, so reading or writing both
23-
//! requires
98+
//! // Inefficient reads and writes - each bool is assigned a distinct storage slot, so reading or writing both requires
2499
//! // executing `SLOAD` or `SSTORE` twice.
25100
//! #[storage]
26101
//! struct Storage<C> {
27102
//! a: PublicMutable<bool, C>,
28103
//! b: PublicMutable<bool, C>,
29104
//! }
30105
//!
31-
//! // By storing the booleans in a single struct and implementing the Packable trait with tight packing, we can now
106+
//! // By storing the booleans in a single struct and implementing the Packable trait with tight packing, we can instead
32107
//! // read and write both values in a single `SLOAD` or `SSTORE` opcode.
33108
//! struct TwoBooleans {
34109
//! a: bool,

noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ mod test;
5252
///
5353
/// ## Examples
5454
///
55-
/// Declaring a `PublicMutable` in the contract's [`storage`](crate::macros::storage::storage) struct requires
55+
/// Declaring a `PublicMutable` in the contract's [`#[storage]`](crate::macros::storage::storage) struct requires
5656
/// specifying the type `T` that is stored in the variable:
5757
///
5858
/// ```noir
Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
1-
/// A trait that defines the common interface for all Aztec state variables. State variables are variables that can be
2-
/// stored in the contract's storage. Examples of state variables are `Owned`, `PublicMutable`, `Map` or
3-
/// `DelayedPublicMutable`.
4-
///
5-
/// # Type Parameters
6-
///
7-
/// * `N` - A compile-time constant that determines how many storage slots need to be reserved for this state variable.
8-
/// * `Context` - The execution context type (e.g., `PublicContext`, `PrivateContext`, `UtilityContext`). The context
9-
/// determines which methods of the state variable are available and controls whether the variable can be accessed in
10-
/// public, private or utility functions.
1+
/// A container for contract state.
112
///
3+
/// All contract state is stored in state variables, and all state variables implement the `StateVariable` trait. For
4+
/// more information on the different types of state variables see the [`state_vars`](crate::state_vars) module.
125
pub trait StateVariable<let N: u32, Context> {
13-
/// Initializes a new state variable at a given storage slot.
14-
///
15-
/// This function is automatically called within the [`storage`](crate::macros::storage::storage) macro and needs
16-
/// to
17-
/// be manually called only when [`storage_no_init`](crate::macros::storage::storage_no_init) is used.
6+
/// Creates a state variable.
187
///
8+
/// This function is automatically called by aztec-nr as the [`self`](crate::contract_self) object is created. Users
9+
/// are expected to only ever need to invoke it when using the
10+
/// [`#[storage_no_init]`](crate::macros::storage::storage_no_init) macro.
1911
fn new(context: Context, storage_slot: Field) -> Self;
2012

21-
/// Returns the storage slot at which the state variable is placed. Typically used only when testing the contract
22-
/// or when using a lower-level API like `get_notes` function.
13+
/// Returns the state variable's storage slot.
2314
fn get_storage_slot(self) -> Field;
2415
}

0 commit comments

Comments
 (0)