From 628f76440eb9b58f2b75b0e7a0f9439ed4494d52 Mon Sep 17 00:00:00 2001 From: Ciara Nightingale Date: Tue, 11 Nov 2025 17:12:03 +0000 Subject: [PATCH 1/4] update state-vars 1 --- .../framework-description/state-variables.md | 205 +++++++++++------- 1 file changed, 127 insertions(+), 78 deletions(-) diff --git a/docs/pages/aztec-nr/framework-description/state-variables.md b/docs/pages/aztec-nr/framework-description/state-variables.md index 1b58e47..2c6f506 100644 --- a/docs/pages/aztec-nr/framework-description/state-variables.md +++ b/docs/pages/aztec-nr/framework-description/state-variables.md @@ -14,7 +14,9 @@ contract MyContract { } ``` -In Aztec.nr we instead define a `struct` (link to noir structs) that holds all state variables. we call this struct **the storage struct**, and it is identified by having the `#[storage]` macro (link to storage api ref) applied to it. +In Aztec.nr, we define a [`struct`](https://noir-lang.org/docs/noir/concepts/data_types/structs) that holds _all_ state variables. This struct is called **the storage struct**, and it is identified by having the `#[storage]` macro applied to it. + + ```noir use aztec::macros::aztec; @@ -30,15 +32,17 @@ contract MyContract { } ``` -The storage struct can have any name, but it is typically just called `Storage`. this struct must also have a generic type called C or Context - this is unfortunate boilerplate (link to api ref where we explain _why_ this must exist and what it means - which we don't think is something users need to know about, but some might be curious). +The storage struct can have _any_ name, but it is _typically_ named `Storage`. This struct must also have a generic type called `C` or `Context` - this is unfortunate boilerplate. + + -> all contract state must be in a single struct. the #[storage] macro can only be used once +The #[storage] macro can only be used once so all contract state must be in a **single** struct. ### Accessing Storage -the contract's storage is accessed via `self.storage` in any contract function. it will automatically be tailored to the execution context of that function, hiding all methods that cannot be invoked there. +The contract's storage is accessed via `self.storage` in any contract function. It will automatically be tailored to the execution context of that function, hiding all methods that cannot be invoked there. -consider for example a PublicMutable state variable, which is a value that is fully accessible in public functions, but which cannot be read or written in a private function, and which can only be in utility functions +Consider, for example, a `PublicMutable` state variable, which is a value that is fully accessible in public functions, but which cannot be read or written in a private function, only in utility functions: ```rust #[storage] @@ -67,97 +71,128 @@ fn my_utility_function() { ### Storage Slots -each state variable gets assigned a different*storage slot*, a numerical value. They they are used depends on the kind of state variable: for public state variables they are related to slots in the public data tree, and for private state variables they are metadata that gets included in the note hash. The purpose of slots is the same for both domains however: they keep the values of different state values _separate_ so that they do not interfere with one another. +Each state variable gets assigned a different a numerical value for their **storage slot**. How they are used depends on the kind of state variable: + +- Ror public state variables they are related to slots in the public data tree +- For private state variables they are metadata that gets included in the note hash. -storage slots are a low level detail that users don't typically need to concern themselves with. they are automatically allocated to each state variable by aztecnr and don't require any kind of manual intervention. indeed, utilizing storage slots directly can be dangerous as it may accidentally result in data collisions across state variables, or invariants being broken. +The purpose of slots is the same for both domains however: they keep the values of different state values _separate_ so that they do not interfere with one another. -in some advanced use cases however it can be useful to have access to these low-level details (link to api ref on storage slot stuff), such as when implementing contract upgrades (link to docs) or when interacting with protocol contracts (link). +Storage slots are a low-level detail that developers and users don't _typically_ need to concern themselves with. They are automatically allocated to each state variable by Aztec.nr and don't require any kind of manual intervention. This is because utilizing storage slots directly can be dangerous as it may accidentally result in data collisions across state variables, or invariants being broken. -### The `Packable` trait +In some advanced use cases, it can be useful to have access to these low-level details, such as when implementing contract [upgrades](./upgradeable-contracts.md) or when interacting with protocol contracts. + + ## Public State Variables -these are state variables that have _public_ content, that is, everyone on the network can see the values they store. they are pretty much equivalent to Solidity state variables. +These are state variables that have _public_ content: everyone on the network can see the values they store. They can be considered to be equivalent to Solidity state variables. ### Choosing a Public State Variable -because they reside in the network's public storage tree (link to foundational concepts), they can only be written to by public contract functions. it is possible to read _past_ values of a public state variable in a private contract function, but the current values of the network's public state tree are not accessible in those. this means that most public state variables cannot be read from a private function - though there's exceptions. +Public state variables are stored in the network's [public storage tree](../../foundational-topics/state-management.md) and they can only be written to by public contract functions. It is possible to read _historic_ values of a public state variable in a private contract function, but the current values in the network's public state tree are not accessible in private functions. This means that most public state variables cannot be read from a private function, though there are some exceptions. -below is a table comparing certain key properties of the different public state variables aztec-nr offers. +Below is a table comparing the key properties of the different public state variables that Aztec.nr offers: -| state variable | mutable? | readable in private? | writable in private? | example use case | +| State variable | Mutable? | Readable in private? | Writable in private? | Example use case | | ---------------------- | ------------------- | -------------------- | -------------------- | ---------------------------------------------------------------------------------- | -| `PublicMutable` | yes | no | no | configuration of admins, global state (e.g. token total supply, total votes) | -| `PublicImmutable` | no | yes | no | fixed configuration, one-way actions (e.g. initialization settings for a proposal) | -| `DelayedPublicMutable` | yes (after a delay) | yes | no | non time sensitive system configuration | +| `PublicMutable` | yes | no | no | Configuration of admins, global state (e.g. token total supply, total votes) | +| `PublicImmutable` | no | yes | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | +| `DelayedPublicMutable` | yes (after a delay) | yes | no | Non time sensitive system configuration | + + -(make the table have links to sections) +### Public Mutable + -### Public Mutable (link to apiref) +`PublicMutable` is the simplest kind of public state variable: a value that can be read and written. It is essentially the same as a non-`immutable` or `constant` Solidity state variable. -This is the simplest kind of public state variable: a value that can be read and written. It is essentially the same as a non-`immutable` Solidity state variable. +It **cannot be read or written to privately**, but it is possible to call private functions that enqueue a public call in which a `PublicMutable` is accessed. For example, a voting contract may allow private submission of votes which then enqueue a public call in which the vote count, represented as a `PublicMutable`, is incremented. This would let anyone see how many votes have been cast, while preserving the privacy of the account that cast the vote. -It cannot be read or written to privately, but it is possible to call private functions that enqueue a public call (link) in which a `PublicMutable` is accessed. For example, a voting contract may allow private submission of votes which then enqueue a public call in which the vote count, represented as a `PublicMutable`, is incremented. This would let anyone see how many votes have been cast, while preserving the privacy of the account that cast the vote. + -### Public Immutable (link to apiref) +### Public Immutable + -This is a simplified version `PublicMutable`: it's a public state variable that can only be written (initialized) once, at which point it can only be read. Unlike Solidity `immutable` state variables, which must be set in the contract's constructor, a `PublicImmutable` can be initialized _at any point in time_ during the contract's lifecycle - attempts to read it prior to initialization will revert. +`PublicImmutable` is a simplified version `PublicMutable`: it's a public state variable that can only be written (initialized) once, at which point it can only be read. Unlike Solidity `immutable` state variables, which must be set in the contract's constructor, a `PublicImmutable` can be initialized _at any point in time_ during the contract's lifecycle and attempts to read it prior to initialization will revert. Due to the value being immutable, it is also possible to read it during private execution - once a circuit proves that the value was set in the past, it knows it cannot have possibly changed. This makes this state variable suitable for immutable public contract configuration or one-off public actions, such as whether a user has signed up or not. -### Delayed Public Mutable (link to apiref) +### Delayed Public Mutable + -it is sometimes quite problematic to be unable to read public mutable state in private. for example, a decentralized exchange might have a configurable swap fee that some admin sets, but which needs to be read by users in their private swaps. this is where `DelayedPublicMutable` comes in. +It is sometimes quite problematic to be unable to read public mutable state in private. For example, a decentralized exchange might have a configurable swap fee that some admin sets, but which needs to be read by users in their private swaps. this is where `DelayedPublicMutable` comes in. -Delayed Public Mutable is the same as a Public Mutable in that it is a public value that can be read and written, but with a caveat: writes only take effect _after some time delay_. these delays are configurable, but they're typically on the order of a couple hours, if not days, making this state variable unsuitable for actions that must be executed immediately - shut us an emergency shut down. it is these very delays however that enable private contract functions to _read the current value of a public state variable_, which is otherwise typically impossible. +`DelayedPublicMutable` is the same as a `PublicMutable` in that it is a public value that can be read and written, but with a caveat: writes only take effect _after some time delay_. These delays are configurable, but they're typically on the order of a couple hours, if not days, making this state variable unsuitable for actions that must be executed immediately - such as an emergency shut down. It is these very delays that enable private contract functions to _read the current value of a public state variable_, which is otherwise typically impossible. -the existence of minimum delays means that a private function that reads a public value at an anchor block has a guarantee that said historical value will remain the current value until _at least_ some time in the future - before the delay elapses. as long as the transaction gets included in a block before that time (link to the `include_by_timestamp` tx property), the read value is valid. +The existence of minimum delays means that a private function that reads a public value at an anchor block has a guarantee that said historical value will remain the current value until _at least_ some time in the future - before the delay elapses. as long as the transaction gets included in a block before that time (by using the `include_by_timestamp` tx property), the read value is valid. + + ## Private State Variables -these are state variables that have _private_ content, that is, only some people know what is stored in them. these work very differently from public state variables and are unlike anything in languages such as Solidity, since they are built from fundamentally different primitives (notes and nullifiers instead of a key-value updatable public database). +Private state variables that have _private_ content meaning that only some people know what is stored in them. These work _very_ differently from public state variables and are unlike anything in languages such as Solidity, since they are built from fundamentally different primitives (UTXO-based notes and nullifiers instead of a key-value updatable public database). + +There are different types of private state variable, include `PrivateMutable`, but these state variables also need to have a **note type**. -understanding these primitives and how they can be used is key to understanding how private state works. +Let's go through notes and nullifiers and how they can be used so we can understand how private state works. -(table comparing private set and private mutable? not sure what the columns would be) ### Notes and Nullifiers -Just as public state is stored in a single public data tree (equivalent to the key-value store that is contract storage on the EVM), private state is stored in two separate trees that have different properties: the note hash tree and the nullifier tree +Just as public state is stored in a single public data tree (equivalent to the `key-value` store used for state on the EVM), private state is stored in two separate trees: +- The note hash tree: stores hashes of the private data, called notes, which are just structs containing private data, with some methods. +- The nullifier tree: the nullifier for a certain note is deterinistic and presence of the nullifier in the nullifier tree determines that the note has been spent/used. + +Let's go into notes and nullifiers in more detail. #### Notes -notes are user-defined data this meant to be stored privately on the blockchain. a note can represent an amount (e.g. some token balance), an id (e.g. a vote proposal id), an address (e.g. an authorized account), or any kind of private piece of data. + + +Notes are user-defined data that can be stored privately on the blockchain. A note can represent any private data e.g., an amount (e.g. some token balance), an ID (e.g. a vote proposal Id) or an address (e.g. an authorized account). + +They also have some metadata, including a storage slot to avoid collisions with other notes, a `randomness` value that helps hide the content, and an `owner` who can nullify the note (more on this later). -they also have some metadata, including a storage slot to avoid collisions with other notes (link above), a 'randomness' value that helps hide the content, and an owner who can nullify the note (more on this later). + -the note content plus metadata are all hashed together, and it is this hash that gets stored onchain in the note hash tree. the underlying note content (the note hash preimage) is not stored anywhere, and so third parties cannot access it and it remains private. the rightful owner will however be able to use the note in the future by producing the hidden content and showing that its hash is stored onchain as part of a zero-knowledge proof - this is typically referred to as 'reading a note'. +The note content, plus the metadata, are all hashed together, and it is this hash that gets stored onc-hain in the note hash tree. This hash is called a commitment. The underlying note content (the note hash preimage) is not stored anywhere on-chain, and so third parties cannot access it and it remains private. The rightful owner will be able to use the note in the future by proving knowledge of the raw note data, by producing the commitment, and showing that its hash is stored onchain as part of a zero-knowledge proof - this is typically referred to as 'reading a note'. -> note: aztecnr comes with some prebuilt note types, like `UintNote` and `AddressNote`, but users are also free to create their own with the `#[note]` macro. (links) +Note: Aztec.nr comes with some prebuilt note types, including [`UintNote`](https://github.com/AztecProtocol/aztec-packages/tree/08935f75dbc3052ce984add225fc7a0dac863050/noir-projects/aztec-nr/uint-note) and [`AddressNote`](https://github.com/AztecProtocol/aztec-packages/tree/08935f75dbc3052ce984add225fc7a0dac863050/noir-projects/aztec-nr/address-note), but users are also free to create their own with the `#[note]` macro. + + ##### Note Discovery -because notes are private, not even the intended recipient is aware of their existence, and they must be somehow notified. for example, when making a payment and creating a note for the payee with the intended amount, they must be shown the preimage of the note that was inserted in the note hash tree in a given transaction in order to acknowledge the payment. +Notes are private meaning that not even the intended recipient is aware of their existence. So, they must be somehow notified. for example, when making a payment and creating a note for the payee with the intended amount, they must be shown the preimage of the note that was inserted in the note hash tree in a given transaction in order to acknowledge the payment. + +Recipients learning about notes created for them is known as 'note discovery', which is a process Aztec.nr handles efficiently, automatically. It does mean however that when a note is created, a _private message_ with the encrypted content of note is created and needs to be delivered to a recipient via one of multiple means. -recipients learning about notes created for them is known as 'note discovery', which is a process aztecnr handles efficiently automatically. it does mean however that when a note is created, a _message_ with the content of note is created and needs to be delivered to a recipient via one of multiple means (link to messages). + ##### Note Lifecycle -notes are more complicated than regular public state, and so it helps to see the different stages one goes through, and when and where each happens. +Notes are more complicated than public state, and so it helps to see the different stages they go through, and when and where each stage happens. -- creation: an account executing a private contract function creates a new note according to contract logic, e.g. transferring tokens to a recipient. note values (e.g. the token amount) and metadata are set, and the note hash computed and inserted as one of the effects of the transaction (link to protocol docs - tx effects). +- **Creation**: an account executing a private contract function creates a new note according to contract logic, e.g. transferring tokens to a recipient. note values (e.g. the token amount) and metadata are set, and the note hash computed and inserted as one of the effects of the transaction + -- encryption: the content of the note is encrypted with a key only the sender and intended recipient know - no other account can decrypt this message. (link to message layout, how we do secret sharing and encryption) +- **Encryption**: the content of the note is encrypted with a key only the sender and intended recipient know - no other account can decrypt this message. + -- delivery: the encrypted message is delivered to the recipient via some means. options include storing it onchain as a transaction log, or sending it offchain e.g. via email or by having the recipient scan a QR code on the sender's device. (link to delivery details and options) +- **Delivery**: the encrypted message is delivered to the recipient via some means. options include storing it onchain as a transaction log, or sending it offchain e.g. via email or by having the recipient scan a QR code on the sender's device. + -- insertion: the transaction is sent to the network and gets included in a block. the note hash is inserted into the note hash tree - this is visible to the entire network, but the content of the note remains private. (link to protocol details - note hash tree insertion and tx effects) +- **Insertion**: the transaction is sent to the network and gets included in a block. the note hash is inserted into the note hash tree - this is visible to the entire network, but the content of the note remains private. + -- discovery: the recipient processes the encrypted message they were sent, decrypting it and finding the note's content (i.e. the hash preimage). they verify that the note's hash exists on chain in the note hash tree. they store the note's content in their own private database, and can now spend the note. (link to message discovery and processing) +- **Discovery**: the recipient processes the encrypted message they were sent, decrypting it and finding the note's content (i.e. the hash preimage). they verify that the note's hash exists on chain in the note hash tree. they store the note's content in their own private database, and can now spend the note. + -- reading: while executing private contract function, the recipient fetches the note's content and metadata from their private database and shows that its hash exists in the note hash tree as part of the zero-knowledge proof. +- **Reading**: while executing private contract function, the recipient fetches the note's content and metadata from their private database and shows that its hash exists in the note hash tree as part of the zero-knowledge proof. -- nullification: the recipient computes the note's nullifier and inserts it as one of the effects of the transaction (link to prot docs tx effects), preventing the note from being read again. +- **Nullification**: the recipient computes the note's nullifier and inserts it as one of the effects of the transaction + #### Nullifiers @@ -170,17 +205,27 @@ most often, nullifiers are used to mark a note as being spent, which prevents no - determinism: the nullifier **must** be deterministic given a note, so that the same nullifier value is computed every time the note is attempted to be spent. A non-deterministic nullifier would result in a note being spendable more than once because the nullifiers would not be duplicates. - secret: the nullifier **must** not be computable by anyone except the owner, _even by someone that knows the full note content_. This is because some third parties _do_ know the note content: when paying someone and creating a note for them, the payer creates the note on their device and thus has access to all of its data and metadata. -there are multiple ways to compute nullifiers that fulfill this property, but the most widely used one is to have the nullifier be a hash of the note contents concatenated with a private key of the note's owner (link to account keys). These values are immutable, and only the owner knows their private keys, and so both determinism and secrecy are achieved. These nullifiers are sometimes called 'zcash-style nullifiers', because this is the format ZCash uses for theirs. +there are multiple ways to compute nullifiers that fulfill this property, but the most widely used one is to have the nullifier be a hash of the note contents concatenated with a private key of the note's owner. These values are immutable, and only the owner knows their private keys, and so both determinism and secrecy are achieved. These nullifiers are sometimes called 'zcash-style nullifiers', because this is the format ZCash uses for theirs. + + + +### How Aztec.nr Abstracts Private State Variables + +As mentioned in the notes and nullifiers section, implementing a private state variable requires careful coordination of multiple primitives and concepts (creating notes, encrypting, delivering, discovering and processing messages, reading notes and computing their nullifiers). This is why Aztec.nr provides convenient types and functions that handle all of these low-level details in order to allow developers to write safe code without having to build and understand the nitty-gritty details. This is akin to how Solidity developers are not required to know assembly or EVM opcodes to be a successful developer. -### How aztec-nr Abstracts Private State Variables +Developers can create their own notes by applying the `#[note]` macro to a Noir struct to define values that will be stored. Private state variables can then hold these notes and be used to read, write, and deliver note messages to the intended recipient. -and mentioned in notes and nullifiers (link), implementing a private state variable requires careful coordination of multiple primitives and concepts (creating notes, encrypting, delivering, discovering and processing messages, reading notes and computing their nullifiers). aztec-nr provides convenient types and functions that handle all of these low level details in order to allow developers to write safe code without having to understand the nitty-gritty. + -by applying the `#[note]` macro to a noir struct, users can define values that will be storable in notes (link to macro docs. also this is a bit of a lie right now, notes also need an owner and randomness, but they soon wont). private state variables can then hold these notes and be used to read, write, and deliver note messages to the intended recipient. +Advanced developers can also change the default behavior of the notes by: -> advanced users can change this default behavior by either defining their own custom note hash and nullifier functions (link to custom notes), implementing their own state variables (link), or even accessing the note hash and nullifiers tree directly (link). +- Defining custom note hash and nullifier functions +- Defining custom state variable implementations +- Accessing the note hash and nullifiers tree directly -the snippet below shows a contract with two private state variables: an admin address (stored in an `AddressNote`) and a counter of how many calls the admin has made (stored in a `UintNote`). These values will be private and therefore not known except by the accounts that own these notes (the admin). In the `perform_admin_action` private function, the contract checks that it is being called by the correct admin and updates the call count by incrementing it by one. + + +The snippet below shows a contract with two private state variables: an admin address (stored in an `AddressNote`) and a counter of how many calls the admin has made (stored in a `UintNote`). These values will be private and therefore not known by anyone except the accounts that own these notes (the admin). In the `perform_admin_action` private function, the contract checks that it is being called by the correct admin and updates the call count by incrementing it by one. (this is not a real snippet, it's missing some small irrelevant details - but the gist of it is correct) @@ -222,54 +267,58 @@ fn perform_admin_action() { ### Choosing a Private State Variable -due to the complexities of aztec's private state model (link to notes and nullifiers section above), private state variables are not just the same as the public ones except private: they each have their own quirks and trade-offs. understanding these is quite important when it comes to designing private smart contracts and how they'll store data. +Due to the complexities of Aztec's private state model, private state variables do not map 1:1 with public state variables. Understanding these differences between the different private state valirables is important when it comes to designing private smart contracts. -below is a table comparing certain key properties of the different private state variables aztec-nr offers. +Below is a table comparing certain key properties of the different private state variables Aztec.nr offers: -| state variable | mutable? | cost to read? | writable by third parties? | example use case | +| State variable | Mutable? | Cost to read? | Writable by third parties? | Example use case | | ------------------ | -------- | ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- | -| `PrivateMutable` | yes | yes | no | mutable user state only accessible by them (e.g. user settings or keys) | -| `PrivateImmutable` | no | no | no | fixed configuration, one-way actions (e.g. initialization settings for a proposal) | -| `PrivateSet` | yes | yes | yes | aggregated state others can add to, e.g. token balance (set of amount notes), nft collections (set of nft ids) | - -### Private Mutable (link to apiref) - -This is conceptually similar to `PublicMutable` or regular Solidity state variables in that it is a variable that has exactly one value at any point in time that can be read and written. unlike those however, this value is _private_, meaning only the account the value belongs to can read it. - -There are some other key differences when compared to `PublicMutable`. The most important one is that _only one account can read and write the state variable_. It is not possible for example to use a `PrivateMutable` to store user settings and then have some admin account alter these settings. Allowing this would require that the admin know both the current value of the private state variable _and_ the owner's nullifying secret key, both of which are private information. +| `PrivateMutable` | yes | yes | no | Mutable user state only accessible by them (e.g. user settings or keys) | +| `PrivateImmutable` | no | no | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | +| `PrivateSet` | yes | yes | yes | Aggregated state others can add to, e.g. token balance (set of amount notes), nft collections (set of nft ids) | -This also means that `PrivateMutable` cannot be used to store things like token balances, which senders would need to update - that is what `PrivateSet` is for (link). +### Private Mutable + -The second difference with `PublicMutable` is seen on the API, notably: +`PrivateMutable` is conceptually similar to `PublicMutable` and regular Solidity state variables in that it is a variable that has exactly one value at any point in time that can be read and written. However, for `PrivateMutable`: -- an initial value must be set via `initialize` (link to apiref section) -- reading the current value results in the state variable being updated, increasing tx costs and requiring delivery of a note message (link to apiref section) -- there is no `write` function - the current value is instead `replace`d (link to apiref section) +- The value is, of course, _private_, meaning only the account the value belongs to can read it. +- _Only ONE account can read and write the state variable_. It is not possible for example to use a `PrivateMutable` to store user settings and then have some admin account alter these settings. Allowing this would require that the admin know both the current value of the private state variable _and_ the owner's nullifying secret key, both of which are private information. This also means that `PrivateMutable` **cannot** be used to store things like token balances, which token senders would need to update. Instead, `PrivateSet` is used. +- There are API differences: + - An initial value must be set via `initialize` + - Reading the current value results in the state variable being updated, increasing tx costs and requiring delivery of a note message + - There is no `write` function - the current value is instead `replace`d -### Private Immutable (link to apiref) + +### Private Immutable + -This is the private equivalent of `PublicImmutable` , except the value is only known to its owner. Like `PublicImmutable`, `PrivateImmutable` can be initialized _at any point in time_ during the contract's lifecycle - attempts to read it prior to initialization will result in failed transactions. +This is the private equivalent of `PublicImmutable`, except the value is only known to its owner. Like `PublicImmutable`, `PrivateImmutable` can be initialized _at any point in time_ during the contract's lifecycle - attempts to read it prior to initialization will result in a failed transaction. `PrivateImmutable` is convenient in that it creates no transaction effects (like notes, nullifiers or messages) when being read. This makes this state variable very convenient for immutable private configuration, such as account contract signing keys. -### Private Set (link to apiref) +### Private Set + -Like `PrivateMutable`, this is a private state variable that can be mutated. There are two key differences however: a `PrivateSet` is not a single value but a _set_ (a collection) of values (represented by notes), and any account can insert values into someone else's set. +Like `PrivateMutable`, this is a private state variable that can be modified. There are two key differences: +- A `PrivateSet` is not a single value but a _set_ (a collection) of values (represented by notes) +- Any account can insert values into someone else's set. -the set's current value is simply the collection of notes in the set that have not yet been nullified. these notes can have any meaning: they could be nft ids, representing a user's nft collection, or they might be token amounts, in which case _the sum_ of all values in the set would be the user's current balance. +The set's current value is the collection of notes in the set that have not yet been nullified. These notes can have any type: they could be nft IDs, representing a user's nft collection, or they might be token amounts, in which case _the sum_ of all values in the set would be the user's current balance. -aggregated state, like a token user balance as a private set of value notes, benefits greatly from third parties having the capacity to insert into the set. any account can create a note for a recipient (e.g. as part of a token transfer), effectively increasing their balance, _without knowing what the total balance is_ (which would be the case if using `PrivateMutable`). this closely mirrors how fiat cash works (people are given bills/notes without knowledge of the sender of their total wealth), and is also very similar to Bitcoin's UTXO model (except private) or Zcash's notes and nullifiers. +Aggregated state, like a token user balance as a `PrivateSet` of `ValueNote`s, benefits greatly from third parties having the capacity to insert into the set. Any account can create a note for a recipient (e.g. as part of a token transfer), effectively increasing their balance, _without knowing what the total balance is_ (which would be the case if using `PrivateMutable`). This closely mirrors how fiat cash works (people are given bills/notes without knowledge of the sender of their total wealth), and is also very similar to Bitcoin's UTXO model (except private) or Zcash's notes and nullifiers. -> note: while the contents of the set are private in the general sense, _some_ accounts do know some of its contents. For example, if account A sends a note of value 20 to B, A will know that at some point in time B held a balance of at least 20. A will however not be able to know when B spends their note due to nullifiers being secret (link to nullifs above). +**Note**: while the contents of the set is private, _some_ accounts do know some of its contents. For example, if account A sends a note of value `20` to B, A will know that at some point in time B held a balance of at least `20`. However, A _will not_ know when B spends the note as they won't know the nullifier since it is derived using the note owner's nullifier secret. -while users can read any number of values from the set, it is **not possible to guarantee all values have been read**. for example, a user might choose not to reveal some notes, and because they are private this cannot be detected. +While users can read any number of values from the set, it is **not possible to guarantee all values have been read**. For example, a user might choose not to reveal some notes, and because they are private this cannot be detected. ## Containers -these are not state variables themselves, but rather components that store multiple state variables according to some logic +Containers are not state variables themselves, but rather store multiple state variables according to some logic. ### Map -A key-value container that maps keys to state variables - just like Solidity's `mapping`. It can be used with any state variable to create independent instances for each key. +A `Map` is a key-value container that maps keys to state variables - just like Solidity's `mapping`. It can be used with any state variable to create independent instances for each key. -For example, a `Map>` can be accessed with an address to obtain a `PublicMutable` that corresponds to it. This is exactly equivalent to a Solidity `mapping (address => uint)`. +For example, a `Map>` can be accessed with an address to obtain +the `PublicMutable` that corresponds to it. This is exactly equivalent to a Solidity `mapping (address => uint)`. From 2d0e1c6f9cfe3bb51f43d9d2a1ecf4af9427b45b Mon Sep 17 00:00:00 2001 From: Ciara Nightingale Date: Tue, 11 Nov 2025 17:44:03 +0000 Subject: [PATCH 2/4] add more info to priavet set --- .../framework-description/state-variables.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/pages/aztec-nr/framework-description/state-variables.md b/docs/pages/aztec-nr/framework-description/state-variables.md index 2c6f506..d60fd80 100644 --- a/docs/pages/aztec-nr/framework-description/state-variables.md +++ b/docs/pages/aztec-nr/framework-description/state-variables.md @@ -207,7 +207,7 @@ most often, nullifiers are used to mark a note as being spent, which prevents no there are multiple ways to compute nullifiers that fulfill this property, but the most widely used one is to have the nullifier be a hash of the note contents concatenated with a private key of the note's owner. These values are immutable, and only the owner knows their private keys, and so both determinism and secrecy are achieved. These nullifiers are sometimes called 'zcash-style nullifiers', because this is the format ZCash uses for theirs. - + ### How Aztec.nr Abstracts Private State Variables @@ -215,7 +215,7 @@ As mentioned in the notes and nullifiers section, implementing a private state v Developers can create their own notes by applying the `#[note]` macro to a Noir struct to define values that will be stored. Private state variables can then hold these notes and be used to read, write, and deliver note messages to the intended recipient. - + Advanced developers can also change the default behavior of the notes by: @@ -308,9 +308,15 @@ The set's current value is the collection of notes in the set that have not yet Aggregated state, like a token user balance as a `PrivateSet` of `ValueNote`s, benefits greatly from third parties having the capacity to insert into the set. Any account can create a note for a recipient (e.g. as part of a token transfer), effectively increasing their balance, _without knowing what the total balance is_ (which would be the case if using `PrivateMutable`). This closely mirrors how fiat cash works (people are given bills/notes without knowledge of the sender of their total wealth), and is also very similar to Bitcoin's UTXO model (except private) or Zcash's notes and nullifiers. -**Note**: while the contents of the set is private, _some_ accounts do know some of its contents. For example, if account A sends a note of value `20` to B, A will know that at some point in time B held a balance of at least `20`. However, A _will not_ know when B spends the note as they won't know the nullifier since it is derived using the note owner's nullifier secret. +**Notes**: -While users can read any number of values from the set, it is **not possible to guarantee all values have been read**. For example, a user might choose not to reveal some notes, and because they are private this cannot be detected. +1. While the contents of the set is private, _some_ accounts do know some of its contents. For example, if account A sends a note of value `20` to B, A will know that at some point in time B held a balance of at least `20`. However, A _will not_ know when B spends the note as they won't know the nullifier since it is derived using the note owner's nullifier secret. + +2. While users can read any number of values from the set, it is **not possible to prove that all values have been read**. This works similarly to physical cash: you can prove someone has _at least_ a certain amount (because they've shown it to you), but you cannot prove that's _all_ they have—they might have hidden some cash in their socks. With `PrivateSet` a user could run modified software that doesn't return all notes when a contract asks. You're only proving that certain notes exist, not that you've revealed the complete set. + +For example, if you tried to implement a system that charges fees to users with balances over `100`, a user could simply hide some of their balance notes to appear under the threshold. Similarly, if you gave users a note that says "you've done this action once, next time pay a 10% fee," they could just not reveal that note. + +This limitation means certain design patterns cannot be implemented with a `PrivateSet` alone and require additional mechanisms like counted sets (which track totals), nullifiers, or public state. ## Containers From e4d4a207d5572d02033fdac14a8fe171dfbf13a9 Mon Sep 17 00:00:00 2001 From: Ciara Nightingale Date: Tue, 11 Nov 2025 18:15:39 +0000 Subject: [PATCH 3/4] modify nullifier section --- .../framework-description/state-variables.md | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/pages/aztec-nr/framework-description/state-variables.md b/docs/pages/aztec-nr/framework-description/state-variables.md index d60fd80..99a9d3a 100644 --- a/docs/pages/aztec-nr/framework-description/state-variables.md +++ b/docs/pages/aztec-nr/framework-description/state-variables.md @@ -109,6 +109,8 @@ Below is a table comparing the key properties of the different public state vari It **cannot be read or written to privately**, but it is possible to call private functions that enqueue a public call in which a `PublicMutable` is accessed. For example, a voting contract may allow private submission of votes which then enqueue a public call in which the vote count, represented as a `PublicMutable`, is incremented. This would let anyone see how many votes have been cast, while preserving the privacy of the account that cast the vote. +```noir +``` ### Public Immutable @@ -118,6 +120,9 @@ It **cannot be read or written to privately**, but it is possible to call privat Due to the value being immutable, it is also possible to read it during private execution - once a circuit proves that the value was set in the past, it knows it cannot have possibly changed. This makes this state variable suitable for immutable public contract configuration or one-off public actions, such as whether a user has signed up or not. +```noir +``` + ### Delayed Public Mutable @@ -127,6 +132,8 @@ It is sometimes quite problematic to be unable to read public mutable state in p The existence of minimum delays means that a private function that reads a public value at an anchor block has a guarantee that said historical value will remain the current value until _at least_ some time in the future - before the delay elapses. as long as the transaction gets included in a block before that time (by using the `include_by_timestamp` tx property), the read value is valid. +```noir +``` ## Private State Variables @@ -160,6 +167,8 @@ The note content, plus the metadata, are all hashed together, and it is this has Note: Aztec.nr comes with some prebuilt note types, including [`UintNote`](https://github.com/AztecProtocol/aztec-packages/tree/08935f75dbc3052ce984add225fc7a0dac863050/noir-projects/aztec-nr/uint-note) and [`AddressNote`](https://github.com/AztecProtocol/aztec-packages/tree/08935f75dbc3052ce984add225fc7a0dac863050/noir-projects/aztec-nr/address-note), but users are also free to create their own with the `#[note]` macro. +```noir +``` ##### Note Discovery @@ -168,44 +177,46 @@ Notes are private meaning that not even the intended recipient is aware of their Recipients learning about notes created for them is known as 'note discovery', which is a process Aztec.nr handles efficiently, automatically. It does mean however that when a note is created, a _private message_ with the encrypted content of note is created and needs to be delivered to a recipient via one of multiple means. +```noir +``` ##### Note Lifecycle Notes are more complicated than public state, and so it helps to see the different stages they go through, and when and where each stage happens. -- **Creation**: an account executing a private contract function creates a new note according to contract logic, e.g. transferring tokens to a recipient. note values (e.g. the token amount) and metadata are set, and the note hash computed and inserted as one of the effects of the transaction +- **Creation**: an account executing a private contract function creates a new note according to contract logic, e.g. transferring tokens to a recipient. The note values (e.g. the token amount) and metadata are set, and the note hash computed and inserted as one of the effects of the transaction. -- **Encryption**: the content of the note is encrypted with a key only the sender and intended recipient know - no other account can decrypt this message. +- **Encryption**: the content of the note is encrypted with a key only the sender and intended recipient know - no other account can decrypt this message. -- **Delivery**: the encrypted message is delivered to the recipient via some means. options include storing it onchain as a transaction log, or sending it offchain e.g. via email or by having the recipient scan a QR code on the sender's device. +- **Delivery**: the encrypted message is delivered to the recipient via some means. Options include storing it onchain as a transaction log, or sending it offchain e.g. via email or by having the recipient scan a QR code on the sender's device. -- **Insertion**: the transaction is sent to the network and gets included in a block. the note hash is inserted into the note hash tree - this is visible to the entire network, but the content of the note remains private. +- **Insertion**: the transaction is sent to the network and gets included in a block. The note hash is inserted into the note hash tree - this is visible to the entire network, but the content of the note remains private. -- **Discovery**: the recipient processes the encrypted message they were sent, decrypting it and finding the note's content (i.e. the hash preimage). they verify that the note's hash exists on chain in the note hash tree. they store the note's content in their own private database, and can now spend the note. +- **Discovery**: the recipient processes the encrypted message they were sent, decrypts it to get the note's content (i.e. the hash preimage). They verify that the note's hash exists onchain in the note hash tree. They store the note's content in their own private database, and can now spend the note. -- **Reading**: while executing private contract function, the recipient fetches the note's content and metadata from their private database and shows that its hash exists in the note hash tree as part of the zero-knowledge proof. +- **Reading**: while executing private contract function, the recipient fetches the note's content and metadata from their private database and show that it's hash exists in the note hash tree as part of the zero-knowledge proof. -- **Nullification**: the recipient computes the note's nullifier and inserts it as one of the effects of the transaction +- **Nullification**: the recipient computes the note's nullifier and inserts it as one of the effects of the transaction . #### Nullifiers -the nullifier tree is append-only - if it wasn't, when a note was spent then external observers would notice that the tree leaf inserted in some transaction was modified in a second transaction, therefore linking them together and leaking privacy. it would for example mean that when a user made a payment to a third party, they'd be able to know when the recipient spent the received funds. nullifiers exist to solve the above issue. +The nullifier tree is append-only - if it wasn't, when a note was spent then external observers would notice that the tree leaf inserted in some transaction was modified in a second transaction, therefore linking them together and leaking privacy. It would, for example, mean that when a user made a payment to a third party, they'd be able to know when the recipient spent the received funds. Nullifiers exist to solve the above issue. -a nullifier is a value which indicates a resource has been spent. nullifiers are unique, and the protocol forbids the same nullifier from being inserted into the tree twice. spending the same resource therefore results in a duplicate nullifier, which invalidates the transaction. +A nullifier is a value which indicates a resource has been spent. Nullifiers are unique, and the protocol forbids the same nullifier from being inserted into the tree twice. Spending the same resource therefore results in a duplicate nullifier, which invalidates the transaction. -most often, nullifiers are used to mark a note as being spent, which prevents note double spends. this requires two properties from the function that computes a note's nullifier: +Most often, nullifiers are used to mark a note as being spent, which prevents note double spends. This requires two properties from the function that computes a note's nullifier: -- determinism: the nullifier **must** be deterministic given a note, so that the same nullifier value is computed every time the note is attempted to be spent. A non-deterministic nullifier would result in a note being spendable more than once because the nullifiers would not be duplicates. -- secret: the nullifier **must** not be computable by anyone except the owner, _even by someone that knows the full note content_. This is because some third parties _do_ know the note content: when paying someone and creating a note for them, the payer creates the note on their device and thus has access to all of its data and metadata. +- **Determinism**: the nullifier **must** be deterministic given a note, so that the same nullifier value is computed every time the note is attempted to be spent. A non-deterministic nullifier would result in a note being spendable more than once because the nullifiers would not be duplicates. +- **Secret**: the nullifier **must** _not_ be computable by anyone except the owner, _even by someone that knows the full note content_. This is because some third parties _do_ know the note content, e.g., when paying someone and creating a note for them, the payer creates the note on their device and thus has access to all of its data and metadata. -there are multiple ways to compute nullifiers that fulfill this property, but the most widely used one is to have the nullifier be a hash of the note contents concatenated with a private key of the note's owner. These values are immutable, and only the owner knows their private keys, and so both determinism and secrecy are achieved. These nullifiers are sometimes called 'zcash-style nullifiers', because this is the format ZCash uses for theirs. +There are multiple ways to compute nullifiers that fulfill this property, but the most widely used one is to have the nullifier be a **hash of the note contents concatenated with a private key of the note's owner**. These values are **immutable**, and only the owner knows their private keys, and so both determinism and secrecy are achieved. These nullifiers are sometimes called 'zcash-style nullifiers', because this is the format ZCash uses. From 639da731c949a39c25481f149b9bb4b9330783ac Mon Sep 17 00:00:00 2001 From: Ciara Nightingale Date: Wed, 3 Dec 2025 16:40:21 +0000 Subject: [PATCH 4/4] add examples for each --- .../framework-description/state-variables.md | 532 ++++++++++++------ 1 file changed, 371 insertions(+), 161 deletions(-) diff --git a/docs/pages/aztec-nr/framework-description/state-variables.md b/docs/pages/aztec-nr/framework-description/state-variables.md index 99a9d3a..bebbc6d 100644 --- a/docs/pages/aztec-nr/framework-description/state-variables.md +++ b/docs/pages/aztec-nr/framework-description/state-variables.md @@ -1,12 +1,12 @@ # State Variables -A contract's state is defined by multiple values, e.g. in a token it'd be the total supply, user balances, outstanding approvals, accounts with minting permission, etc. each of these persisting values is called a _state variable_. +A contract's state is defined by multiple values, e.g. in a token it'd be the total supply, user balances, outstanding approvals, accounts with minting permission, etc. Each of these persisting values is called a _state variable_. -One of the first design considerations for any smart contract is how it'll store its state. this is doubly true in aztec due to there being both public and private state - the tradeoff space is large, so there's room for lots of decisions. +One of the first design considerations for any smart contract is how it'll store its state. This is doubly true in Aztec due to there being **both public and private state** - the tradeoff space is large, so there's room for lots of decisions. ## The Storage Struct -State variables are declared in solidity by simply listing them inside of the contract, like so: +State variables are declared in Solidity by simply listing them inside of the contract, like so: ```solidity contract MyContract { @@ -16,9 +16,7 @@ contract MyContract { In Aztec.nr, we define a [`struct`](https://noir-lang.org/docs/noir/concepts/data_types/structs) that holds _all_ state variables. This struct is called **the storage struct**, and it is identified by having the `#[storage]` macro applied to it. - - -```noir +```rust use aztec::macros::aztec; #[aztec] @@ -27,22 +25,21 @@ contract MyContract { #[storage] struct Storage { - // state variables go here + // state variables go here e.g, the admin of the contract + admin: PublicMutable, } } ``` -The storage struct can have _any_ name, but it is _typically_ named `Storage`. This struct must also have a generic type called `C` or `Context` - this is unfortunate boilerplate. +The storage struct can have _any_ name, but it is _typically_ named `Storage`. This struct must also have a generic type called `C` or `Context` - this is an unfortunate boilerplate parameter that provides execution mode information. - - -The #[storage] macro can only be used once so all contract state must be in a **single** struct. +The `#[storage]` macro can only be used once so all contract state must be in a **single** struct. ### Accessing Storage The contract's storage is accessed via `self.storage` in any contract function. It will automatically be tailored to the execution context of that function, hiding all methods that cannot be invoked there. -Consider, for example, a `PublicMutable` state variable, which is a value that is fully accessible in public functions, but which cannot be read or written in a private function, only in utility functions: +Consider, for example, a `PublicMutable` state variable, which is a value that is fully accessible in public functions, read-only in utility functions and not accessible in a private function: ```rust #[storage] @@ -69,28 +66,13 @@ fn my_utility_function() { } ``` -### Storage Slots - -Each state variable gets assigned a different a numerical value for their **storage slot**. How they are used depends on the kind of state variable: - -- Ror public state variables they are related to slots in the public data tree -- For private state variables they are metadata that gets included in the note hash. - -The purpose of slots is the same for both domains however: they keep the values of different state values _separate_ so that they do not interfere with one another. - -Storage slots are a low-level detail that developers and users don't _typically_ need to concern themselves with. They are automatically allocated to each state variable by Aztec.nr and don't require any kind of manual intervention. This is because utilizing storage slots directly can be dangerous as it may accidentally result in data collisions across state variables, or invariants being broken. - -In some advanced use cases, it can be useful to have access to these low-level details, such as when implementing contract [upgrades](./upgradeable-contracts.md) or when interacting with protocol contracts. - - - ## Public State Variables These are state variables that have _public_ content: everyone on the network can see the values they store. They can be considered to be equivalent to Solidity state variables. ### Choosing a Public State Variable -Public state variables are stored in the network's [public storage tree](../../foundational-topics/state-management.md) and they can only be written to by public contract functions. It is possible to read _historic_ values of a public state variable in a private contract function, but the current values in the network's public state tree are not accessible in private functions. This means that most public state variables cannot be read from a private function, though there are some exceptions. +Public state variables are stored in the network's public storage tree and they can only be written to by public contract functions. It is possible to read _historic_ values of a public state variable in a private contract function, but the current values in the network's public state tree are not accessible in private functions. This means that most public state variables cannot be read from a private function, though there are some exceptions that are documented in the table below. Below is a table comparing the key properties of the different public state variables that Aztec.nr offers: @@ -100,242 +82,470 @@ Below is a table comparing the key properties of the different public state vari | `PublicImmutable` | no | yes | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | | `DelayedPublicMutable` | yes (after a delay) | yes | no | Non time sensitive system configuration | - - -### Public Mutable - +### PublicMutable `PublicMutable` is the simplest kind of public state variable: a value that can be read and written. It is essentially the same as a non-`immutable` or `constant` Solidity state variable. It **cannot be read or written to privately**, but it is possible to call private functions that enqueue a public call in which a `PublicMutable` is accessed. For example, a voting contract may allow private submission of votes which then enqueue a public call in which the vote count, represented as a `PublicMutable`, is incremented. This would let anyone see how many votes have been cast, while preserving the privacy of the account that cast the vote. -```noir +#### Declaration + +Store mutable public state using `PublicMutable` for values that need to be updated throughout the contract's lifecycle. + +```rust +#[storage] +struct Storage { + admin: PublicMutable, + total_supply: PublicMutable, +} +``` + +To add a group of `authorized_users` that are able to perform actions in our contract in public storage: + +```rust +#[storage] +struct Storage { + authorized_users: Map, Context>, +} +``` + +#### `read` + +`PublicMutable` variables have a `read` method to read the value at the location in storage: + +```rust +#[external("public")] +fn check_admin() { + let admin = self.storage.admin.read(); + assert(admin == self.msg_sender().unwrap(), "caller is not admin"); +} ``` - -### Public Immutable - +#### `write` + +The `write` method on `PublicMutable` variables takes the value to write as an input and saves this in storage: + +```rust +#[external("public")] +fn set_admin(new_admin: AztecAddress) { + self.storage.admin.write(new_admin); +} +``` + +### PublicImmutable `PublicImmutable` is a simplified version `PublicMutable`: it's a public state variable that can only be written (initialized) once, at which point it can only be read. Unlike Solidity `immutable` state variables, which must be set in the contract's constructor, a `PublicImmutable` can be initialized _at any point in time_ during the contract's lifecycle and attempts to read it prior to initialization will revert. Due to the value being immutable, it is also possible to read it during private execution - once a circuit proves that the value was set in the past, it knows it cannot have possibly changed. This makes this state variable suitable for immutable public contract configuration or one-off public actions, such as whether a user has signed up or not. -```noir +#### Declaration + +```rust +#[storage] +struct Storage { + contract_version: PublicImmutable, +} +``` + +#### `initialize` + +This function sets the immutable value. It can only be called once. + +```rust +#[external("public")] +fn initialize_version(version: u32) { + self.storage.contract_version.initialize(version); +} +``` + +:::warning +A `PublicImmutable`'s storage **must** only be set once via `initialize`. Attempting to override this by manually accessing the underlying storage slots breaks all properties of the data structure, rendering it useless. +::: + +#### `read` + +Returns the stored immutable value. This function is available in public, private and utility contexts. + +```rust +#[external("public")] +fn get_version() -> u32 { + self.storage.contract_version.read() +} ``` -### Delayed Public Mutable - +### DelayedPublicMutable -It is sometimes quite problematic to be unable to read public mutable state in private. For example, a decentralized exchange might have a configurable swap fee that some admin sets, but which needs to be read by users in their private swaps. this is where `DelayedPublicMutable` comes in. +It is sometimes necessary to read public mutable state in private. For example, a decentralized exchange might have a configurable swap fee that some admin sets, but which needs to be read by users in their private swaps. This is where `DelayedPublicMutable` comes in. `DelayedPublicMutable` is the same as a `PublicMutable` in that it is a public value that can be read and written, but with a caveat: writes only take effect _after some time delay_. These delays are configurable, but they're typically on the order of a couple hours, if not days, making this state variable unsuitable for actions that must be executed immediately - such as an emergency shut down. It is these very delays that enable private contract functions to _read the current value of a public state variable_, which is otherwise typically impossible. -The existence of minimum delays means that a private function that reads a public value at an anchor block has a guarantee that said historical value will remain the current value until _at least_ some time in the future - before the delay elapses. as long as the transaction gets included in a block before that time (by using the `include_by_timestamp` tx property), the read value is valid. +The existence of minimum delays means that a private function that reads a public value at an anchor block has a guarantee that said historical value will remain the current value until _at least_ some time in the future - before the delay elapses. As long as the transaction gets included in a block before that time (by using the `include_by_timestamp` tx property), the read value is valid. + +#### Declaration + +Unlike other state variables, `DelayedPublicMutable` receives not only a type parameter for the underlying datatype, but also a `DELAY` type parameter with the value change delay as a number of seconds. + +```rust +global MY_DELAY: u32 = 3600; // 1 hour delay + +#[storage] +struct Storage { + swap_fee: DelayedPublicMutable, +} +``` -```noir +#### `schedule_value_change` + +This is the means by which a `DelayedPublicMutable` variable mutates its contents. It schedules a value change for the variable at a future timestamp after the `DELAY` has elapsed. + +```rust +#[external("public")] +fn set_swap_fee(new_fee: u128) { + assert(self.storage.admin.read() == self.msg_sender().unwrap(), "caller is not admin"); + self.storage.swap_fee.schedule_value_change(new_fee); +} +``` + +#### `get_current_value` + +Returns the current value in a public, private or utility execution context. + +```rust +#[external("private")] +fn use_swap_fee() { + let current_fee = self.storage.swap_fee.get_current_value(); + // Use the fee in calculations +} ``` - ## Private State Variables -Private state variables that have _private_ content meaning that only some people know what is stored in them. These work _very_ differently from public state variables and are unlike anything in languages such as Solidity, since they are built from fundamentally different primitives (UTXO-based notes and nullifiers instead of a key-value updatable public database). +Private state variables have _private_ content meaning that only some people know what is stored in them. These work _very_ differently from public state variables and are unlike anything in languages such as Solidity, since they are built from fundamentally different primitives (UTXO-based notes and nullifiers instead of a key-value updatable public database). -There are different types of private state variable, include `PrivateMutable`, but these state variables also need to have a **note type**. +Aztec.nr provides three private state variable types: -Let's go through notes and nullifiers and how they can be used so we can understand how private state works. +- `PrivateMutable`: Single mutable private value +- `PrivateImmutable`: Single immutable private value +- `PrivateSet`: Collection of private notes +But, you'll notice that each requires a `NoteType`. To understand this, let's go through notes and nullifiers and how they can be used so we can understand how private state works. ### Notes and Nullifiers -Just as public state is stored in a single public data tree (equivalent to the `key-value` store used for state on the EVM), private state is stored in two separate trees: -- The note hash tree: stores hashes of the private data, called notes, which are just structs containing private data, with some methods. -- The nullifier tree: the nullifier for a certain note is deterinistic and presence of the nullifier in the nullifier tree determines that the note has been spent/used. +Just as public state is stored in a single public data tree (equivalent to the `key-value` store used for state on the EVM), private state is stored in two separate trees: -Let's go into notes and nullifiers in more detail. +- The note hash tree: stores hashes of the private data, called notes, which are just structs containing private data, with some methods. +- The nullifier tree: the nullifier for a certain note is deterministic and presence of the nullifier in the nullifier tree determines that the note has been spent/used. #### Notes - - Notes are user-defined data that can be stored privately on the blockchain. A note can represent any private data e.g., an amount (e.g. some token balance), an ID (e.g. a vote proposal Id) or an address (e.g. an authorized account). -They also have some metadata, including a storage slot to avoid collisions with other notes, a `randomness` value that helps hide the content, and an `owner` who can nullify the note (more on this later). - - +They also have some metadata, including a storage slot to avoid collisions with other notes, a `randomness` value that helps hide the content, and an `owner` who can nullify the note. -The note content, plus the metadata, are all hashed together, and it is this hash that gets stored onc-hain in the note hash tree. This hash is called a commitment. The underlying note content (the note hash preimage) is not stored anywhere on-chain, and so third parties cannot access it and it remains private. The rightful owner will be able to use the note in the future by proving knowledge of the raw note data, by producing the commitment, and showing that its hash is stored onchain as part of a zero-knowledge proof - this is typically referred to as 'reading a note'. +The note content, plus the metadata, are all hashed together, and it is this hash that gets stored on-chain in the note hash tree. This hash is called a commitment. The underlying note content (the note hash preimage) is not stored anywhere on-chain, and so third parties cannot access it and it remains private. Note: Aztec.nr comes with some prebuilt note types, including [`UintNote`](https://github.com/AztecProtocol/aztec-packages/tree/08935f75dbc3052ce984add225fc7a0dac863050/noir-projects/aztec-nr/uint-note) and [`AddressNote`](https://github.com/AztecProtocol/aztec-packages/tree/08935f75dbc3052ce984add225fc7a0dac863050/noir-projects/aztec-nr/address-note), but users are also free to create their own with the `#[note]` macro. -```noir -``` - +#### Nullifiers -##### Note Discovery +A nullifier is a value which indicates a resource has been spent. Nullifiers are unique, and the protocol forbids the same nullifier from being inserted into the tree twice. Spending the same resource therefore results in a duplicate nullifier, which invalidates the transaction. -Notes are private meaning that not even the intended recipient is aware of their existence. So, they must be somehow notified. for example, when making a payment and creating a note for the payee with the intended amount, they must be shown the preimage of the note that was inserted in the note hash tree in a given transaction in order to acknowledge the payment. +Most often, nullifiers are used to mark a note as being spent, which prevents note double spends. The nullifier is typically computed as a **hash of the note contents concatenated with a private key of the note's owner**. These values are **immutable**, and only the owner knows their private keys, ensuring both determinism and secrecy. -Recipients learning about notes created for them is known as 'note discovery', which is a process Aztec.nr handles efficiently, automatically. It does mean however that when a note is created, a _private message_ with the encrypted content of note is created and needs to be delivered to a recipient via one of multiple means. +### Note Emissions -```noir -``` - +When working with private state variables, many operations return a `NoteEmission` type rather than the note directly. This is a type-safe wrapper that ensures you explicitly decide whether to emit the note for the recipient to discover, or to discard it. -##### Note Lifecycle +#### Why NoteEmission? -Notes are more complicated than public state, and so it helps to see the different stages they go through, and when and where each stage happens. +Private notes need to be communicated to their recipients so they know the note exists and can use it. The `NoteEmission` wrapper forces you to make an explicit choice about how this happens: -- **Creation**: an account executing a private contract function creates a new note according to contract logic, e.g. transferring tokens to a recipient. The note values (e.g. the token amount) and metadata are set, and the note hash computed and inserted as one of the effects of the transaction. - +- **`.emit(recipient, MessageDelivery)`**: Emits the note so the recipient can discover it. You must specify: + - `recipient`: The `AztecAddress` who should receive the note + - `MessageDelivery`: Either `CONSTRAINED_ONCHAIN` (verified in the circuit) or `UNCONSTRAINED_ONCHAIN` (cheaper but less secure) -- **Encryption**: the content of the note is encrypted with a key only the sender and intended recipient know - no other account can decrypt this message. - +- **`.discard()`**: Explicitly discards the note without emitting it (useful when you're reading but not sending) -- **Delivery**: the encrypted message is delivered to the recipient via some means. Options include storing it onchain as a transaction log, or sending it offchain e.g. via email or by having the recipient scan a QR code on the sender's device. - +#### Accessing the Note -- **Insertion**: the transaction is sent to the network and gets included in a block. The note hash is inserted into the note hash tree - this is visible to the entire network, but the content of the note remains private. - +After calling `.emit()` or `.discard()`, you can access the underlying note using the `.note` property: -- **Discovery**: the recipient processes the encrypted message they were sent, decrypts it to get the note's content (i.e. the hash preimage). They verify that the note's hash exists onchain in the note hash tree. They store the note's content in their own private database, and can now spend the note. - +```rust +// Get the note and emit it +let emission = self.storage.user_settings.at(owner).get_note(); +let note = emission.emit(owner, MessageDelivery.CONSTRAINED_ONCHAIN).note; -- **Reading**: while executing private contract function, the recipient fetches the note's content and metadata from their private database and show that it's hash exists in the note hash tree as part of the zero-knowledge proof. +// Or discard it if you don't need to emit +let note = self.storage.user_settings.at(owner).get_note().discard().note; +``` -- **Nullification**: the recipient computes the note's nullifier and inserts it as one of the effects of the transaction . - +Methods that return `NoteEmission` include `initialize()`, `get_note()`, and `replace()` on `PrivateMutable` and `PrivateImmutable`, as well as `insert()` on `PrivateSet`. -#### Nullifiers +### Choosing a Private State Variable -The nullifier tree is append-only - if it wasn't, when a note was spent then external observers would notice that the tree leaf inserted in some transaction was modified in a second transaction, therefore linking them together and leaking privacy. It would, for example, mean that when a user made a payment to a third party, they'd be able to know when the recipient spent the received funds. Nullifiers exist to solve the above issue. +Due to the complexities of Aztec's private state model, private state variables do not map 1:1 with public state variables. Understanding these differences between the different private state variables is important when it comes to designing private smart contracts. -A nullifier is a value which indicates a resource has been spent. Nullifiers are unique, and the protocol forbids the same nullifier from being inserted into the tree twice. Spending the same resource therefore results in a duplicate nullifier, which invalidates the transaction. +Below is a table comparing certain key properties of the different private state variables Aztec.nr offers: -Most often, nullifiers are used to mark a note as being spent, which prevents note double spends. This requires two properties from the function that computes a note's nullifier: +| State variable | Mutable? | Cost to read? | Writable by third parties? | Example use case | +| ------------------ | -------- | ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `PrivateMutable` | yes | yes | no | Mutable user state only accessible by them (e.g. user settings or keys) | +| `PrivateImmutable` | no | no | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | +| `PrivateSet` | yes | yes | yes | Aggregated state others can add to, e.g. token balance (set of amount notes), nft collections (set of nft ids) | -- **Determinism**: the nullifier **must** be deterministic given a note, so that the same nullifier value is computed every time the note is attempted to be spent. A non-deterministic nullifier would result in a note being spendable more than once because the nullifiers would not be duplicates. -- **Secret**: the nullifier **must** _not_ be computable by anyone except the owner, _even by someone that knows the full note content_. This is because some third parties _do_ know the note content, e.g., when paying someone and creating a note for them, the payer creates the note on their device and thus has access to all of its data and metadata. +### PrivateMutable -There are multiple ways to compute nullifiers that fulfill this property, but the most widely used one is to have the nullifier be a **hash of the note contents concatenated with a private key of the note's owner**. These values are **immutable**, and only the owner knows their private keys, and so both determinism and secrecy are achieved. These nullifiers are sometimes called 'zcash-style nullifiers', because this is the format ZCash uses. +`PrivateMutable` is conceptually similar to `PublicMutable` and regular Solidity state variables in that it is a variable that has exactly one value at any point in time that can be read and written. However, for `PrivateMutable`: - +- The value is, of course, _private_, meaning only the account the value belongs to can read it. +- _Only ONE account can read and write the state variable_. It is not possible for example to use a `PrivateMutable` to store user settings and then have some admin account alter these settings. +- Reading the current value results in the state variable being updated, increasing tx costs and requiring delivery of a note message. +- There is no `write` function - the current value is instead `replace`d. -### How Aztec.nr Abstracts Private State Variables +#### Declaration -As mentioned in the notes and nullifiers section, implementing a private state variable requires careful coordination of multiple primitives and concepts (creating notes, encrypting, delivering, discovering and processing messages, reading notes and computing their nullifiers). This is why Aztec.nr provides convenient types and functions that handle all of these low-level details in order to allow developers to write safe code without having to build and understand the nitty-gritty details. This is akin to how Solidity developers are not required to know assembly or EVM opcodes to be a successful developer. +```rust +#[storage] +struct Storage { + user_settings: PrivateMutable, +} +``` -Developers can create their own notes by applying the `#[note]` macro to a Noir struct to define values that will be stored. Private state variables can then hold these notes and be used to read, write, and deliver note messages to the intended recipient. +#### `is_initialized` - +An unconstrained method to check whether the `PrivateMutable` has been initialized or not: -Advanced developers can also change the default behavior of the notes by: +```rust +let is_initialized = self.storage.user_settings.at(owner).is_initialized(); +``` -- Defining custom note hash and nullifier functions -- Defining custom state variable implementations -- Accessing the note hash and nullifiers tree directly +#### `initialize` - +The `PrivateMutable` should be initialized to create the first note and value: + +```rust +use aztec::messages::message_delivery::MessageDelivery; -The snippet below shows a contract with two private state variables: an admin address (stored in an `AddressNote`) and a counter of how many calls the admin has made (stored in a `UintNote`). These values will be private and therefore not known by anyone except the accounts that own these notes (the admin). In the `perform_admin_action` private function, the contract checks that it is being called by the correct admin and updates the call count by incrementing it by one. +#[external("private")] +fn initialize_settings(value: u8) { + let owner = self.msg_sender(); + let note = SettingsNote::new(value, owner); + self.storage.user_settings.at(owner).initialize(note).emit(owner, MessageDelivery.CONSTRAINED_ONCHAIN); +} +``` -(this is not a real snippet, it's missing some small irrelevant details - but the gist of it is correct) +#### `get_note` + +This function allows us to get the note of a `PrivateMutable`, essentially reading the value: ```rust -#[note] -struct AddressNote { - value: AztecAddress, +#[external("private")] +fn read_settings() -> SettingsNote { + let owner = self.msg_sender(); + self.storage.user_settings.at(owner).get_note().emit(owner, MessageDelivery.CONSTRAINED_ONCHAIN).note } +``` -#[note] -struct UintNote { - value: u128, -} +:::info +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. +::: -#[storage] -struct Storage { - admin: PrivateImmutable, - admin_call_count: PrivateMutable, -} +#### `replace` +To update the value of a `PrivateMutable`, we can use the `replace` method: + +```rust #[external("private")] -fn perform_admin_action() { - // Read the contract's admin address and check against the caller - let admin = self.storage.admin.read().value; - assert(self.msg_sender() == admin); - - // Update the call count by replacing (updating - rename soon) the current note with a new one that equals the - // current value + 1 - this requires knowing what the current value is in the first place, i.e. reading the variable. - // - // We then deliver the encrypted message with the note's content to the admin, so that they become aware of the new - // value of the counter and can update it again in the future. - self.storage.admin_call_count - .replace(|current| UintNote{ value: current.value + 1 }) // wouldn't it be great if we didn't have to deal with this wrapping and unwrapping? - .deliver(admin); - - ... +fn update_settings(new_value: u8) { + let owner = self.msg_sender(); + self.storage.user_settings.at(owner).replace(|_| SettingsNote::new(new_value, owner)).emit(owner, MessageDelivery.CONSTRAINED_ONCHAIN); } ``` -### Choosing a Private State Variable +### PrivateImmutable -Due to the complexities of Aztec's private state model, private state variables do not map 1:1 with public state variables. Understanding these differences between the different private state valirables is important when it comes to designing private smart contracts. +`PrivateImmutable` represents a unique private state variable that, as the name suggests, is immutable. Once initialized, its value cannot be altered. This is the private equivalent of `PublicImmutable`, except the value is only known to its owner. -Below is a table comparing certain key properties of the different private state variables Aztec.nr offers: +#### Declaration -| State variable | Mutable? | Cost to read? | Writable by third parties? | Example use case | -| ------------------ | -------- | ------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- | -| `PrivateMutable` | yes | yes | no | Mutable user state only accessible by them (e.g. user settings or keys) | -| `PrivateImmutable` | no | no | no | Fixed configuration, one-way actions (e.g. initialization settings for a proposal) | -| `PrivateSet` | yes | yes | yes | Aggregated state others can add to, e.g. token balance (set of amount notes), nft collections (set of nft ids) | +```rust +#[storage] +struct Storage { + signing_key: PrivateImmutable, +} +``` -### Private Mutable - +#### `initialize` -`PrivateMutable` is conceptually similar to `PublicMutable` and regular Solidity state variables in that it is a variable that has exactly one value at any point in time that can be read and written. However, for `PrivateMutable`: +When this function is invoked, it creates a nullifier for the storage slot, ensuring that the `PrivateImmutable` cannot be initialized again: -- The value is, of course, _private_, meaning only the account the value belongs to can read it. -- _Only ONE account can read and write the state variable_. It is not possible for example to use a `PrivateMutable` to store user settings and then have some admin account alter these settings. Allowing this would require that the admin know both the current value of the private state variable _and_ the owner's nullifying secret key, both of which are private information. This also means that `PrivateMutable` **cannot** be used to store things like token balances, which token senders would need to update. Instead, `PrivateSet` is used. -- There are API differences: - - An initial value must be set via `initialize` - - Reading the current value results in the state variable being updated, increasing tx costs and requiring delivery of a note message - - There is no `write` function - the current value is instead `replace`d +```rust +#[external("private")] +fn initialize_key(key_value: Field) { + let owner = self.msg_sender(); + let note = KeyNote::new(key_value, owner); + self.storage.signing_key.at(owner).initialize(note).emit(owner, MessageDelivery.CONSTRAINED_ONCHAIN); +} +``` - -### Private Immutable - +#### `get_note` + +Similar to the `PrivateMutable`, we can use the `get_note` method to read the value: + +```rust +#[external("private")] +fn get_key() -> KeyNote { + let owner = self.msg_sender(); + self.storage.signing_key.at(owner).get_note() +} +``` -This is the private equivalent of `PublicImmutable`, except the value is only known to its owner. Like `PublicImmutable`, `PrivateImmutable` can be initialized _at any point in time_ during the contract's lifecycle - attempts to read it prior to initialization will result in a failed transaction. +Unlike a `PrivateMutable`, the `get_note` function for a `PrivateImmutable` doesn't nullify the current note. This means that multiple accounts can concurrently call this function to read the value. -`PrivateImmutable` is convenient in that it creates no transaction effects (like notes, nullifiers or messages) when being read. This makes this state variable very convenient for immutable private configuration, such as account contract signing keys. +### PrivateSet -### Private Set - +`PrivateSet` is used for managing a collection of notes. Like `PrivateMutable`, this is a private state variable that can be modified. There are two key differences: -Like `PrivateMutable`, this is a private state variable that can be modified. There are two key differences: - A `PrivateSet` is not a single value but a _set_ (a collection) of values (represented by notes) - Any account can insert values into someone else's set. The set's current value is the collection of notes in the set that have not yet been nullified. These notes can have any type: they could be nft IDs, representing a user's nft collection, or they might be token amounts, in which case _the sum_ of all values in the set would be the user's current balance. -Aggregated state, like a token user balance as a `PrivateSet` of `ValueNote`s, benefits greatly from third parties having the capacity to insert into the set. Any account can create a note for a recipient (e.g. as part of a token transfer), effectively increasing their balance, _without knowing what the total balance is_ (which would be the case if using `PrivateMutable`). This closely mirrors how fiat cash works (people are given bills/notes without knowledge of the sender of their total wealth), and is also very similar to Bitcoin's UTXO model (except private) or Zcash's notes and nullifiers. +#### Declaration -**Notes**: +For example, to add a mapping of private token balances to storage: -1. While the contents of the set is private, _some_ accounts do know some of its contents. For example, if account A sends a note of value `20` to B, A will know that at some point in time B held a balance of at least `20`. However, A _will not_ know when B spends the note as they won't know the nullifier since it is derived using the note owner's nullifier secret. +```rust +#[storage] +struct Storage { + balances: Map, Context>, +} +``` + +#### `insert` -2. While users can read any number of values from the set, it is **not possible to prove that all values have been read**. This works similarly to physical cash: you can prove someone has _at least_ a certain amount (because they've shown it to you), but you cannot prove that's _all_ they have—they might have hidden some cash in their socks. With `PrivateSet` a user could run modified software that doesn't return all notes when a contract asks. You're only proving that certain notes exist, not that you've revealed the complete set. +Allows us to modify the storage by inserting a note into the `PrivateSet`: -For example, if you tried to implement a system that charges fees to users with balances over `100`, a user could simply hide some of their balance notes to appear under the threshold. Similarly, if you gave users a note that says "you've done this action once, next time pay a 10% fee," they could just not reveal that note. +```rust +#[external("private")] +fn mint_tokens(to: AztecAddress, amount: u128) { + let note = UintNote::new(amount, to); + self.storage.balances.at(to).insert(note).emit(to, MessageDelivery.UNCONSTRAINED_ONCHAIN); +} +``` -This limitation means certain design patterns cannot be implemented with a `PrivateSet` alone and require additional mechanisms like counted sets (which track totals), nullifiers, or public state. +#### `get_notes` -## Containers +Retrieves notes the account has access to. You can optionally provide filtering options: -Containers are not state variables themselves, but rather store multiple state variables according to some logic. +```rust +// Get all notes (with default options) +let options = NoteGetterOptions::new(); +let notes = self.storage.balances.at(owner).get_notes(options); + +// Or with custom options (e.g., limit the number of notes) +let options = NoteGetterOptions::new().set_limit(5); +let notes = self.storage.balances.at(owner).get_notes(options); +``` + +#### `pop_notes` + +This function pops (gets, removes and returns) the notes the account has access to: + +```rust +// Pop notes with a limit +let options = NoteGetterOptions::new().set_limit(10); +let notes = self.storage.balances.at(owner).pop_notes(options); +``` + +#### `remove` + +Will remove a note from the `PrivateSet` if it previously has been read from storage: + +```rust +self.storage.balances.at(owner).remove(note); +``` + +## Containers ### Map A `Map` is a key-value container that maps keys to state variables - just like Solidity's `mapping`. It can be used with any state variable to create independent instances for each key. -For example, a `Map>` can be accessed with an address to obtain -the `PublicMutable` that corresponds to it. This is exactly equivalent to a Solidity `mapping (address => uint)`. +For example, a `Map>` can be accessed with an address to obtain the `PublicMutable` that corresponds to it. This is exactly equivalent to a Solidity `mapping (address => uint)`. + +#### Declaration + +```rust +#[storage] +struct Storage { + // Map of addresses to public balances + public_balances: Map, Context>, + + // Map of addresses to private note sets + private_balances: Map, Context>, +} +``` + +#### Usage + +Use the `.at()` method to access values by key: + +```rust +#[external("public")] +fn increase_balance(account: AztecAddress, amount: u128) { + let current = self.storage.public_balances.at(account).read(); + self.storage.public_balances.at(account).write(current + amount); +} +``` + +## Custom Structs in Public Storage + +Both `PublicMutable` and `PublicImmutable` are generic over any serializable type, which means you can store custom structs in public storage. + +### Define a Custom Struct + +To use a custom struct in public storage, it must implement the `Packable` trait: + +```rust +use dep::aztec::protocol_types::{ + address::AztecAddress, + traits::{Deserialize, Packable, Serialize} +}; + +#[derive(Deserialize, Packable, Serialize)] +pub struct Asset { + pub interest_accumulator: u128, + pub last_updated_ts: u64, + pub loan_to_value: u128, + pub oracle: AztecAddress, +} +``` + +### Store and Use Custom Structs + +```rust +#[storage] +struct Storage { + assets: Map, Context>, +} + +#[external("public")] +fn update_asset(asset_id: Field, new_accumulator: u128) { + let mut asset = self.storage.assets.at(asset_id).read(); + asset.interest_accumulator = new_accumulator; + self.storage.assets.at(asset_id).write(asset); +} +``` + +## Storage Slots + +Each state variable gets assigned a different numerical value for their **storage slot**. How they are used depends on the kind of state variable: + +- For public state variables, storage slots are related to slots in the public data tree +- For private state variables, storage slots are metadata that gets included in the note hash + +The purpose of slots is the same for both domains: they keep the values of different state values _separate_ so that they do not interfere with one another. + +Storage slots are a low-level detail that developers don't typically need to concern themselves with. They are automatically allocated to each state variable by Aztec.nr.