aztec-nr - noir_aztec::state_vars::private_set

Struct PrivateSet

pub struct PrivateSet<Note, Context> {
    pub context: Context,
    pub storage_slot: Field,
    pub owner: AztecAddress,
}

PrivateSet is an owned state variable type, which enables you to read, mutate, and write private state. Because it is "owned," you must wrap a PrivateSet inside an Owned state variable when storing it:

E.g.:

#[storage]
struct Storage {
    your_variable: Owned, Context>,
}

For more details on what "owned" means, see the documentation for the [OwnedStateVariable] trait.

The PrivateSet facilitates: the insertion of new notes, the reading of existing notes, and the nullification of
existing notes.

The "current value" of a PrivateSet is the collection of all _not-yet-nullified_ account's notes in the set.


## Example.

A user's token balance can be represented as a PrivateSet of multiple notes,
where the note type contains a value.
The "current value" of the user's token balance (the PrivateSet state variable)
can be interpreted as the summation of the values contained within all
not-yet-nullified notes (aka "current notes") in the PrivateSet.

This is similar to a physical wallet containing five $10 notes: the owner's
wallet balance is the sum of all those $10 notes: $50.
To spend $2, they can get one $10 note, nullify it, and insert one $8 note as
change. Their new wallet balance will then be interpreted as the new summation: $48.

The interpretation doesn't always have to be a "summation of values". When
`get_notes` is called, PrivateSet does not attempt to interpret the notes at all;
it's up to the custom code of the smart contract to make an interpretation.

For example: a set of notes could instead represent a moving average; or a modal
value; or some other single statistic. Or the set of notes might not be
collapsible into a single statistic: it could be a disjoint collection of NFTs
which are housed under the same "storage slot".

It's worth noting that a user can prove existence of _at least_ some subset
of notes in a PrivateSet, but they cannot prove existence of _all_ notes
in a PrivateSet.
The physical wallet is a good example: a user can prove that there are five
$10 notes in their wallet by furnishing those notes. But because we cannot
_see_ the entirety of their wallet, they might have many more notes that
they're choosing to not showing us.

## When to choose PrivateSet vs PrivateMutable:

- If you want _someone else_ (other than the owner of the private state) to be
  able to make edits (insert notes).
- If you don't want to leak the storage_slot being initialized (see the
  PrivateMutable file).
- If you find yourself needing to re-initialize a PrivateMutable (see that file).

The 'current' value of a _PrivateMutable_ state variable is only ever represented
by _one_ note at a time. To mutate the current value of a PrivateMutable, the
current note always gets nullified, and a new, replacement note gets inserted.
So if nullification is always required to mutate a PrivateMutable, that means
only the 'owner' of a given PrivateMutable state variable can ever mutate it.
For some use cases, this can be too limiting: A key feature of some smart contract
functions is that _multiple people_ are able to mutate a particular state
variable.

PrivateSet enables "other people" (other than the owner of the private state) to
mutate the 'current' value, with some limitations:
The 'owner' is still the only person with the ability to `remove` notes from the
the set.
"Other people" can `insert` notes into the set.

## Privacy

The methods of a PrivateSet are only executable in a PrivateContext, and are
designed to not leak anything about _which_ state variable was read/modified/
inserted, to the outside world.

## Struct Fields:

* context - The execution context (PrivateContext or UtilityContext).
* storage_slot -  All notes that "belong" to a given PrivateSet state variable
                  are augmented with a common `storage_slot` field, as a way of
  identifying which set they belong to. (Management of `storage_slot` is handled
  within the innards of the PrivateSet impl, so you shouldn't need to think about
  this any further).


## Generic Parameters:

* `Note` - Many notes of this type will collectively form the PrivateSet at the
           given storage_slot.
* `Context` - The execution context (PrivateContext or UtilityContext).

Fields

context: Context
storage_slot: Field

Implementations

impl<Note> PrivateSet<Note, &mut PrivateContext>

pub fn insert(self, note: Note) -> NoteMessage<Note>
where Note: Packable, Note: NoteType, Note: NoteHash, Note: Eq

Inserts a new note into the PrivateSet.

Arguments

  • note - A newly-created note that you would like to insert into this PrivateSet.

Returns

  • NoteMessage - A type-safe wrapper which makes it clear to the smart contract dev that they now have a choice: they need to decide whether they would like to send the contents of the newly- created note to someone, or not. If they would like to, they have some further choices:
    • What kind of log to use? (Private log, or offchain log).
    • What kind of encryption scheme to use? (Currently only AES128 is supported)
    • Whether to constrain delivery of the note, or not. At the moment, aztec-nr provides limited options. You can call .deliver() on the returned type to encrypt and log the note. See NoteMessage for more details.

    Note: We're planning a significant refactor of this syntax, to make the syntax of how to encrypt and deliver notes much clearer, and to make the default options much clearer to developers. We will also be enabling easier ways to customize your own note encryption options.

Advanced:

Ultimately, this function inserts the note into the protocol's Note Hash Tree. Behind the scenes, we do the following:

  • Augment the note with the storage_slot of this PrivateSet, to convey which set it belongs to.
  • Augment the note with a note_type_id, so that it can be correctly filed- away when it is eventually discovered, decrypted, and processed by its intended recipient. (The note_type_id is usually allocated by the #[note] macro).
  • Provide the contents of the (augmented) note to the PXE, so that it can store all notes created by the user executing this function.
    • The note is also kept in the PXE's memory during execution, in case this newly-created note gets read in some later execution frame of this transaction. In such a case, we feed hints to the kernel to squash: the so-called "transient note", its note log (if applicable), and the nullifier that gets created by the reading function.
  • Hash the (augmented) note into a single Field, via the note's own compute_note_hash method.
  • Push the note_hash to the PrivateContext. From here, the protocol's kernel circuits will take over and insert the note_hash into the protocol's "note hash tree".
    • Before insertion, the protocol will:
      • "Silo" the note_hash with the contract_address of the calling function, to yield a siloed_note_hash. This prevents state collisions between different smart contracts.
      • Ensure uniqueness of the siloed_note_hash, to prevent Faerie-Gold attacks, by hashing the siloed_note_hash with a unique value, to yield a unique_siloed_note_hash (see the protocol spec for more).
pub fn pop_notes<PreprocessorArgs, FilterArgs, let M: u32>( self, options: NoteGetterOptions<Note, M, PreprocessorArgs, FilterArgs>, ) -> BoundedVec<Note, 16>
where Note: Packable<N = M>, Note: NoteType, Note: NoteHash, Note: Eq

Pops a collection of "current" notes (i.e. not-yet-nullified notes) which belong to this PrivateSet.

"Pop" indicates that, conceptually, the returned notes will get permanently removed (nullified) from the PrivateSet by this method.

The act of nullifying convinces us that the returned notes are indeed "current" (because if they can be nullified, it means they haven't been nullified already, because a note can only be nullified once).

This means that -- whilst the returned notes should be considered "current" within the currently-executing execution frame of the tx -- they will be not be considered "current" by any later execution frame of this tx (or any future tx).

Notes will be selected from the PXE's database, via an oracle call, according to the filtering options provided.

Arguments

  • options - See NoteGetterOptions. Enables the caller to specify the properties of the notes that must be returned by the oracle call to the PXE. The NoteGetterOptions are designed to contain functions which constrain that the returned notes do indeed adhere to the specified options. Those functions are executed within this pop_notes call.

Returns

  • BoundedVec<Note, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL>
    • A vector of "current" notes, that have been constrained to satisfy the retrieval criteria specified by the given options.

Generic Parameters

  • PreprocessorArgs - See NoteGetterOptions.
  • FilterArgs - See NoteGetterOptions.
  • M - The length of the note (in Fields), when packed by the Packable trait.

Advanced:

Reads the notes:

  • Gets notes from the PXE, via an oracle call, according to the filtering options provided.
  • Constrains that the returned notes do indeed adhere to the options. (Note: the options contain constrained functions that get invoked within this function).
  • Asserts that the notes do indeed belong to this calling function's contract_address, and to this PrivateSet's storage_slot.
  • Computes the note_hash for each note, using the storage_slot and contract_address of this PrivateSet instance.
  • Asserts that the note_hash does indeed exist:
    • For settled notes: makes a request to the kernel to perform a merkle membership check against the historical Note Hashes Tree that this tx is referencing.
    • For transient notes: makes a request to the kernel to ensure that the note was indeed emitted by some earlier execution frame of this tx.

Nullifies the notes:

  • Computes the nullifier for each note.
    • (The nullifier computation differs depending on whether the note is settled or transient).
  • Pushes the nullifiers to the PrivateContext. From here, the protocol's kernel circuits will take over and insert the nullifiers into the protocol's "nullifier tree".
    • Before insertion, the protocol will:
      • "Silo" each nullifier with the contract_address of the calling function, to yield a siloed_nullifier. This prevents nullifier collisions between different smart contracts.
      • Ensure that each siloed_nullifier does not already exist in the nullifier tree. The nullifier tree is an indexed merkle tree, which supports efficient non-membership proofs.
pub fn remove(self, retrieved_note: RetrievedNote<Note>)
where Note: NoteType, Note: NoteHash, Note: Eq

Permanently removes (conceptually) the given note from this PrivateSet, by nullifying it.

Note that if you obtained the note via get_notes it's much better to use pop_notes, as pop_notes results in significantly fewer constraints, due to avoiding an extra hash and read request check.

Arguments

  • retrieved_note - A note which -- earlier in the calling function's execution -- has been retrieved from the PXE. The retrieved_note is constrained to have been read from the i

Returns

  • NoteMessage - A type-safe wrapper which makes it clear to the smart contract dev that they now have a choice: they need to decide whether they would like to send the contents of the newly- created note to someone, or not. If they would like to, they have some further choices:
    • What kind of log to use? (Private log, or offchain log).
    • What kind of encryption scheme to use? (Currently only AES128 is supported)
    • Whether to constrain delivery of the note, or not. At the moment, aztec-nr provides limited options. See NoteMessage for further details.

    Note: We're planning a significant refactor of this syntax, to make the syntax of how to encrypt and deliver notes much clearer, and to make the default options much clearer to developers. We will also be enabling easier ways to customize your own note encryption options.

pub fn get_notes<PreprocessorArgs, FilterArgs, let M: u32>( self, options: NoteGetterOptions<Note, M, PreprocessorArgs, FilterArgs>, ) -> BoundedVec<RetrievedNote<Note>, 16>
where Note: Packable<N = M>, Note: NoteType, Note: NoteHash, Note: Eq

Returns a filtered collection of notes from the set.

DANGER: the returned notes do not get nullified within this get_notes function, and so they cannot necessarily be considered "current" notes. I.e. you might be reading notes that have already been nullified. It is this which distinguishes get_notes from pop_notes.

Note that if you later on remove the note it's much better to use pop_notes as pop_notes results in significantly fewer constrains due to avoiding 1 read request check. If you need for your app to see the notes before it can decide which to nullify (which ideally would not be the case, and you'd be able to rely on the filter and preprocessor to do this), then you have no resort but to call get_notes and then remove.

Notes will be selected from the PXE's database, via an oracle call, according to the filtering options provided.

Arguments

  • options - See NoteGetterOptions. Enables the caller to specify the properties of the notes that must be returned by the oracle call to the PXE. The NoteGetterOptions are designed to contain functions which constrain that the returned notes do indeed adhere to the specified options. Those functions are executed within this pop_notes call.

Returns

  • BoundedVec<Note, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL>
    • A vector of "current" notes, that have been constrained to satisfy the retrieval criteria specified by the given options.

Generic Parameters

  • PreprocessorArgs - See NoteGetterOptions.
  • FilterArgs - See NoteGetterOptions.
  • M - The length of the note (in Fields), when packed by the Packable trait.

Advanced:

Reads the notes:

  • Gets notes from the PXE, via an oracle call, according to the filtering options provided.
  • Constrains that the returned notes do indeed adhere to the options. (Note: the options contain constrained functions that get invoked within this function).
  • Asserts that the notes do indeed belong to this calling function's contract_address, and to this PrivateSet's storage_slot.
  • Computes the note_hash for each note, using the storage_slot and contract_address of this PrivateSet instance.
  • Asserts that the note_hash does indeed exist:
    • For settled notes: makes a request to the kernel to perform a merkle membership check against the historical Note Hashes Tree that this tx is referencing.
    • For transient notes: makes a request to the kernel to ensure that the note was indeed emitted by some earlier execution frame of this tx.

impl<Note> PrivateSet<Note, UtilityContext>

pub unconstrained fn view_notes( self, options: NoteViewerOptions<Note, <Note as Packable>::N>, ) -> BoundedVec<Note, 10>
where Note: Packable, Note: NoteType, Note: NoteHash, Note: Eq

Returns a collection of notes which belong to this PrivateSet, according to the given selection options.

Notice that this function is executable only within a UtilityContext, which is an unconstrained environment on the user's local device.

Arguments

  • options - See NoteGetterOptions. Enables the caller to specify the properties of the notes that must be returned by the oracle call to the PXE.

Trait implementations

impl<Context, Note> OwnedStateVariable<Context> for PrivateSet<Note, Context>

pub fn new(context: Context, storage_slot: Field, owner: AztecAddress) -> Self