aztec-nr - noir_aztec::state_vars

Struct PublicImmutable

pub struct PublicImmutable<T, Context>
{ /* private fields */ }

Immutable public values.

This is one of the most basic public state variables. It is similar to an immutable or constant Solidity state variable.

It represents a public value of type T that can be initialized just once during the lifetime of the contract, allowing this single value to be read.

Unlike Solidity's immutable or constant, a PublicImmutable's initialization does not need to occur during contract initialization - it can happen at any point in time (but only once). This also makes it possible to have a Map of PublicImmutables.

Access Patterns

A value stored in a PublicImmutable can be read and initialized from public contract functions.

Unlike PublicMutable it is also possible to read a PublicImmutable from a private contract function, though it is not possible to initialize one. A common pattern is to have these functions enqueue a public self calls in which the initialization operation is performed.

For a mutable (with restrictions) variant which also can be read from private functions see DelayedPublicMutable.

Privacy

PublicImmutable provides zero privacy in terms of the value stored and any public accesses: the entire network can see these and the data involved.

Reading a PublicImmutable from a private contract function however is completely private, and leaks zero information about the fact that the value was read.

Use Cases

This is suitable for any kind of fixed global configuration that needs to be accessible by private contract functions, such as token decimals, related contracts in a multi-contract configuration, etc.

It is also useful for fixed per-user configuration by combining it with a Map, e.g. a registry of their account types.

PublicImmutable's main limitation is the immutability, which in many cases leads to DelayedPublicMutable being used instead. But in those cases where fixed values are not a problem, this is a fine choice for storage.

Examples

Declaring a PublicImmutable in the the contract's storage struct requires specifying the type T that is stored in the variable:

#[storage]
struct Storage<Context> {
    decimals: PublicImmutable<u8, Context>,

    account_types: Map<AztecAddress, PublicImmutable<AccountType, Context>, Context>,
}

Requirements

The type T stored in the PublicImmutable must implement the Packable trait.

Implementation Details

Values are packed and stored directly in the public storage tree, along with the hash of the packed representation. A PublicImmutable therefore takes up as many storage slots as the packing length of the stored type T, plus one. This hash allows for efficient private reads, such that only a single public storage value is read. For more details, see WithHash.

An initialization nullifier prevents re-initialization of the PublicImmutable. This allows reading an initialized PublicImmutable from a private contract function, since the value is guaranteed to not change.

Private contract functions however cannot determine that a PublicImmutable has not been initialized, as they do not have access to the current network state, only the past state at the anchor block. They can perform historical non-inclusion proofs of the initialization nullifier at past times, but they have no way to guarantee that it has not been emitted since then.

Implementations

impl<T> PublicImmutable<T, PublicContext>

pub fn initialize(self, value: T)
where T: Packable, T: Eq

Stores a permanent value.

This function can only be called once on a given PublicImmutable: subsequent calls will fail with a duplicate nullifier.

Examples

Contract initialization:

#[external("public")]
#[initializer]
fn initialize(decimals: u8) {
    self.storage.decimals.iniitalize(decimals);
}

Non-initializer initialization:

// Can only be called once per account
#[external("public")]
fn set_account_type(account_type: AccountType) {
    self.storage.account_types.at(self.msg_sender()).iniitalize(account_type);
}

Cost

The SSTORE AVM opcode is invoked a number of times equal to T's packed length plus one, and the EMITNULLIFIER AVM opcode is invoked once.

pub fn read(self) -> T
where T: Packable, T: Eq

Reads the permanent value.

This function reverts the transaction if the PublicImmutable is not initialized.

Examples

A public getter that returns the permanent value:

#[external("public")]
fn get_decimals() -> u8 {
    self.storage.decimals.read()
}

Cost

The SLOAD AVM opcode is invoked a number of times equal to T's packed length, and the NULLIFIEREXISTS AVM opcode is invoked once.

pub fn read_unsafe(self) -> T
where T: Packable, T: Eq

Reads the permanent value, skipping the initialization check.

This is cheaper than PublicImmutable::read, but it should only be used if the PublicImmutable is known to be initialized.

If the PublicImmutable is not initialized, this returns the default empty public storage value, which is all zeroes - equivalent to let t = T::unpack(std::mem::zeroed());.

Examples

A public getter that returns the permanent value:

#[external("public")]
fn get_decimals() -> u8 {
    // This call is safe because `decimals` is initialized in the contract's initializer function
    self.storage.decimals.read_unsafe()
}

Cost

The SLOAD AVM opcode is invoked a number of times equal to T's packed length.

pub fn is_initialized(self) -> bool

Returns true if the PublicImmutable has been initialized.

Examples:

Conditional initialization:

#[external("public")]
fn set_account_type_if_not_set(account_type: AccountType) {
    if !self.storage.account_types.at(self.msg_sender()).is_initialized() {
        self.storage.account_types.at(self.msg_sender()).iniitalize(account_type);
    }
}

Cost

The NULLIFIEREXISTS AVM opcode is invoked once.

impl<T> PublicImmutable<T, UtilityContext>

pub unconstrained fn read(self) -> T
where T: Packable, T: Eq

Reads the permanent value.

This function throws if the PublicImmutable is not initialized.

Examples

#[external("utility")]
fn get_decimals() -> u8 {
    self.storage.decimals.read()
}

impl<T> PublicImmutable<T, &mut PrivateContext>

pub fn read(self) -> T
where T: Packable, T: Eq

Reads the permanent value.

This function throws if the PublicImmutable is not initialized.

Examples

#[external("private")]
fn get_decimals() -> u8 {
    self.storage.decimals.read()
}

Cost

A historical public storage read at the anchor block is performed for a single storage slot, regardless of T's packed length. This is because PublicImmutable::initialize stores not just the value but also its hash: this function obtains the preimage from an oracle and proves that it matches the hash from public storage.

Because of this reason it is convenient to group together all of a contract's public immutable values that are read privately in a single type T:

// Bad: reading both `decimals` and `symbol` will require two historical public storage reads
#[storage]
struct Storage<Context> {
    decimals: PublicImmutable<u8, Context>,
    symbol: PubicImmutable<FieldCompressedString, Context>,
}

// Good: both `decimals` and `symbol` are retrieved in a single historical public storage read
#[derive(Packable)]
struct Config {
    decimals: u8,
    symbol: FieldCompressedString,
}

#[storage]
struct Storage<Context> {
    config: PublicImmutable<Config, Context>,
}

impl<Context, T> PublicImmutable<T, Context>

pub fn compute_initialization_inner_nullifier(self) -> Field

Returns the inner nullifier emitted during initialization.

Trait implementations

impl<Context, let M: u32, T> StateVariable<M + 1, Context> for PublicImmutable<T, Context>
where T: Packable<N = M>

pub fn new(context: Context, storage_slot: Field) -> Self pub fn get_storage_slot(self) -> Field