How to Define Functions
Overview
This guide shows you how to define different types of functions in your Aztec contracts, each serving specific purposes and execution environments.
Quick reference
| Annotation | Execution | State access |
|---|---|---|
#[external("private")] | User device | Private state (and selected public values via storage types) |
#[external("public")] | Sequencer | Public state |
#[external("utility")] | Offchain client | Public + private (unconstrained) |
#[internal("private")] | N/A | Inlined private helper (non-entrypoint) |
#[internal("public")] | N/A | Inlined public helper (non-entrypoint) |
#[view] | Private or public | Read-only (no state mutation) |
#[only_self] | Private or public | Callable only by the same contract |
#[initializer] | Private or public | One-time initialization |
Prerequisites
- An Aztec contract project set up with the
aztec-nrdependency - Basic understanding of Noir programming language
- Familiarity with Aztec Protocol's call types (private vs public)
Define private functions
Use #[external("private")] to create functions that execute privately on user devices. For example:
#[external("private")]
fn increment(owner: AztecAddress) {
debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
self.storage.counters.at(owner).add(1).deliver(MessageDelivery.CONSTRAINED_ONCHAIN);
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L36-L42
Private functions run in a private context, can access private state, and can read certain public values through storage types like DelayedPublicMutable.
Define public functions
Use #[external("public")] to create functions that execute on the sequencer:
#[external("public")]
fn mint_public(employee: AztecAddress, amount: u64) {
// Only Giggle can mint tokens
assert_eq(self.msg_sender().unwrap(), self.storage.owner.read(), "Only Giggle can mint BOB tokens");
// Add tokens to employee's public balance
let current_balance = self.storage.public_balances.at(employee).read();
self.storage.public_balances.at(employee).write(current_balance + amount);
}
Source code: docs/examples/contracts/bob_token_contract/src/main.nr#L42-L52
Public functions operate on public state, similar to EVM contracts. They can write to private storage, but any data written from a public function is publicly visible.
Define utility functions
Create offchain query functions using the #[external("utility")] annotation with unconstrained.
Utility functions are standalone unconstrained functions that cannot be called from private or public functions. They are meant to be called by applications to perform auxiliary tasks like querying contract state or processing offchain messages. Example:
#[external("utility")]
unconstrained fn get_counter(owner: AztecAddress) -> pub u128 {
self.storage.counters.at(owner).balance_of()
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L44-L49
Use aztec.js simulate to execute utility functions and read their return values. For details, see Call Types.
Define view functions
Create read-only functions using the #[view] annotation combined with #[external("private")] or #[external("public")]:
#[external("public")]
#[view]
fn get_config_value() -> Field {
// logic
}
View functions cannot modify contract state. They're akin to Ethereum's view functions.
#[view] only applies to #[external("private")] and #[external("public")] functions.
Define only-self functions
Create contract-only functions using the #[only_self] annotation:
#[external("public")]
#[only_self]
fn _assert_is_owner(address: AztecAddress) {
assert_eq(address, self.storage.owner.read(), "Only Giggle can mint BOB tokens");
}
Source code: docs/examples/contracts/bob_token_contract/src/main.nr#L129-L135
Only-self functions are only callable by the same contract, which is useful when a private function enqueues a public call that should only be callable internally.
Define initializer functions
Create constructor-like functions using the #[initializer] annotation:
#[initializer]
#[external("private")]
// We can name our initializer anything we want as long as it's marked as aztec(initializer)
fn initialize(headstart: u64, owner: AztecAddress) {
self.storage.counters.at(owner).add(headstart as u128).deliver(
MessageDelivery.CONSTRAINED_ONCHAIN,
);
}
Source code: docs/examples/contracts/counter_contract/src/main.nr#L25-L34
Use multiple initializers
Define multiple initialization options:
- Mark each function with
#[initializer] - Choose which one to call during deployment
- Any initializer marks the contract as initialized
Define internal functions
Create helper functions using #[internal("private")] or #[internal("public")]. Internal functions are inlined at call sites and do not create separate entrypoints:
#[internal("private")]
fn _prepare_transfer(to: AztecAddress, amount: u128) -> Field {
// helper logic for private functions
}
#[internal("public")]
fn _update_balance(owner: AztecAddress, amount: u128) {
// helper logic for public functions
}
Call internal functions via self.internal:
let result = self.internal._prepare_transfer(recipient, amount);
Key constraints:
- Private internal functions can only be called from private external or internal functions
- Public internal functions can only be called from public external or internal functions