Migration notes
Aztec is in full-speed development. Literally every version breaks compatibility with the previous ones. This page attempts to target errors and difficulties you might encounter when upgrading, and how to resolve them.
TBD
[AVM] Gas cost multipliers for public execution to reach simulation/proving parity
Gas costs for several AVM opcodes have been adjusted with multipliers to better align public simulation costs with actual proving costs.
| Opcode | Multiplier | Previous Cost | New Cost |
|---|---|---|---|
| FDIV | 25x | 9 | 225 |
| SLOAD | 10x | 129 | 1,290 |
| SSTORE | 20x | 1,657 | 33,140 |
| NOTEHASHEXISTS | 4x | 126 | 504 |
| EMITNOTEHASH | 15x | 1,285 | 19,275 |
| NULLIFIEREXISTS | 7x | 132 | 924 |
| EMITNULLIFIER | 20x | 1,540 | 30,800 |
| L1TOL2MSGEXISTS | 5x | 108 | 540 |
| SENDL2TOL1MSG | 2x | 209 | 418 |
| CALL | 3x | 3,312 | 9,936 |
| STATICCALL | 3x | 3,312 | 9,936 |
| GETCONTRACTINSTANCE | 4x | 1,527 | 6,108 |
| POSEIDON2 | 15x | 24 | 360 |
| ECADD | 10x | 27 | 270 |
Impact: Contracts with public bytecode performing any of these operations will see increased gas consumption.
[PXE] deprecated getNotes
This function serves only for debugging purposes so we are taking it out of the main PXE API. If you still need to consume it, you can
do so through the new debug sub-module.
- this.pxe.getNotes(filter);
+ this.pxe.debug.getNotes(filter);
[Aztec node, archiver] Deprecated getPrivateLogs
Aztec node no longer offers a getPrivateLogs method. If you need to process the logs of a block, you can instead use getBlock and call getPrivateLogs on an L2BlockNew instance. See the diff below for before/after equivalent code samples.
- const logs = await aztecNode.getPrivateLogs(blockNumber, 1);
+ const logs = (await aztecNode.getBlock(blockNumber))?.toL2Block().getPrivateLogs();
[Aztec.nr] Private event emission API changes
Private events are still emitted via the emit function, but this now returns an EventMessage type that must have deliver_to called on it in order to deliver the event message to the intended recipients. This allows for multiple recipients to receive the same event.
- self.emit(event, recipient, delivery_method)
+ self.emit(event).delivery(recipient, delivery_method)
[Aztec.nr] History proof functions no longer require storage_slot parameter
The RetrievedNote struct now includes a storage_slot field, making it self-contained for proving note inclusion and validity. As a result, the history proof functions in the aztec::history module no longer require a separate storage_slot parameter.
Affected functions:
BlockHeader::prove_note_inclusion- removedstorage_slot: FieldparameterBlockHeader::prove_note_validity- removedstorage_slot: FieldparameterBlockHeader::prove_note_is_nullified- removedstorage_slot: FieldparameterBlockHeader::prove_note_not_nullified- removedstorage_slot: Fieldparameter
Migration:
The storage_slot is now read from retrieved_note.storage_slot internally. Simply remove the storage_slot argument from all calls to these functions:
let header = context.get_anchor_block_header();
- header.prove_note_inclusion(retrieved_note, storage_slot);
+ header.prove_note_inclusion(retrieved_note);
let header = context.get_anchor_block_header();
- header.prove_note_validity(retrieved_note, storage_slot, context);
+ header.prove_note_validity(retrieved_note, context);
let header = context.get_anchor_block_header();
- header.prove_note_is_nullified(retrieved_note, storage_slot, context);
+ header.prove_note_is_nullified(retrieved_note, context);
let header = context.get_anchor_block_header();
- header.prove_note_not_nullified(retrieved_note, storage_slot, context);
+ header.prove_note_not_nullified(retrieved_note, context);
[Aztec.nr] Note fields are now public
All note struct fields are now public, and the new() constructor methods and getter methods have been removed. Notes should be instantiated using struct literal syntax, and fields should be accessed directly.
The motivation for this change has been enshrining of randomness which lead to the new method being unnecessary boilerplate.
Affected notes:
UintNote-valueis now public,new()andget_value()removedAddressNote-addressis now public,new()andget_address()removedFieldNote-valueis now public,new()andvalue()removed
Migration:
- let note = UintNote::new(100);
+ let note = UintNote { value: 100 };
- let value = note.get_value();
+ let value = note.value;
- let address_note = AddressNote::new(owner);
+ let address_note = AddressNote { address: owner };
- let address = address_note.get_address();
+ let address = address_note.address;
- let field_note = FieldNote::new(42);
+ let field_note = FieldNote { value: 42 };
- let value = field_note.value();
+ let value = field_note.value;
[Aztec.nr] emit renamed to deliver
Private state variable functions that created notes and returned their messages no longer return a NoteEmission but instead a NoteMessage. These messages are delivered to their owner via deliver instead of emit. The verb 'emit' remains for things like emitting events.
- self.storage.balances.at(owner).add(5).emit(owner);
+ self.storage.balances.at(owner).add(5).deliver();
To deliver a message to a different recipient, use deliver_to:
- self.storage.balances.at(owner).add(5).emit(other);
+ self.storage.balances.at(owner).add(5).deliver_to(other);
[Aztec.nr] ValueNote renamed to FieldNote and value-note crate renamed to field-note
The ValueNote struct has been renamed to FieldNote to better reflect that it stores a Field value. The crate has also been renamed from value-note to field-note.
Migration:
- Update your
Nargo.tomldependencies:value_note = { path = "..." }→field_note = { path = "..." } - Update imports:
use value_note::value_note::ValueNote→use field_note::field_note::FieldNote - Update type references:
ValueNote→FieldNote - Update generic parameters:
PrivateSet<ValueNote, ...>→PrivateSet<FieldNote, ...>
[Aztec.nr] New balance-set library for managing token balances
A new balance-set library has been created that provides BalanceSet<Context> for managing u128 token balances with UintNote. This consolidates balance management functionality that was previously duplicated across contracts.
Features:
add(amount: u128)- Add to balancesub(amount: u128)- Subtract from balance (with change note)try_sub(amount: u128, max_notes: u32)- Attempt to subtract with configurable note limitbalance_of()- Get total balance (unconstrained)
Usage:
use balance_set::BalanceSet;
#[storage]
struct Storage<Context> {
balances: Owned<BalanceSet<Context>, Context>,
}
// In a private function:
self.storage.balances.at(owner).add(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN);
self.storage.balances.at(owner).sub(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN);
// In an unconstrained function:
let balance = self.storage.balances.at(owner).balance_of();
[Aztec.nr] EasyPrivateUint deprecated and removed
The EasyPrivateUint type and easy-private-state crate have been deprecated and removed. Use BalanceSet from the balance-set crate instead.
Migration:
- Remove
easy_private_statedependency fromNargo.toml - Add
balance_set = { path = "../../../../aztec-nr/balance-set" }toNargo.toml - Update storage:
EasyPrivateUint<Context>→Owned<BalanceSet<Context>, Context> - Update method calls:
add(amount, owner)→at(owner).add(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN)sub(amount, owner)→at(owner).sub(amount).deliver(owner, MessageDelivery.CONSTRAINED_ONCHAIN)get_value(owner)→at(owner).balance_of()(returnsu128instead ofField)
[Aztec.nr] balance_utils removed from value-note (now field-note)
The balance_utils module has been removed from the field-note crate (formerly value-note). If you need similar functionality, implement it locally in your contract or use BalanceSet for u128 balances.
[Aztec.nr] filter_notes_min_sum removed from value-note (now field-note)
The filter_notes_min_sum function has been removed from the field-note crate (formerly in value-note). If you need this functionality, copy it to your contract locally. This function was only used in specific test contracts and doesn't belong in the general-purpose note library.
[Aztec.nr] derive_ecdh_shared_secret_using_aztec_address removed
This function made it annoying to deal with invalid addresses in circuits. If you were using it, replace it with derive_ecdh_shared_secret instead:
-let shared_secret = derive_ecdh_shared_secret_using_aztec_address(secret, address).unwrap();
+let shared_secret = derive_ecdh_shared_secret(secret, address.to_address_point().unwrap().inner);
[Aztec.nr] Note owner is now enshrined
It turns out that in all the cases a note always have a logical owner. For this reason we have decided to enshrine the concept of a note owner and you should drop the field from your note:
#[derive(Deserialize, Eq, Packable, Serialize)]
#[note]
pub struct ValueNote {
value: Field,
- owner: AztecAddress,
}
The owner being enshrined means that our API explicitly expects it on the input.
The NoteHash trait got modified as follows:
pub trait NoteHash {
fn compute_note_hash(
self,
+ owner: AztecAddress,
storage_slot: Field,
randomness: Field,
) -> Field;
fn compute_nullifier(
self,
context: &mut PrivateContext,
+ owner: AztecAddress,
note_hash_for_nullification: Field,
) -> Field;
unconstrained fn compute_nullifier_unconstrained(
self,
+ owner: AztecAddress,
note_hash_for_nullification: Field,
) -> Field;
}
Our low-level note utilities now also accept owner as a parameter:
pub fn create_note<Note>(
context: &mut PrivateContext,
+ owner: AztecAddress,
storage_slot: Field,
note: Note,
) -> NoteEmission<Note>
where
Note: NoteType + NoteHash + Packable,
{
...
}
Signature of some functions like destroy_note_unsafe is unchanged:
pub fn destroy_note_unsafe<Note>(
context: &mut PrivateContext,
retrieved_note: RetrievedNote<Note>,
note_hash_read: NoteHashRead,
)
where
Note: NoteHash,
{
...
}
because RetrievedNote now contains owner.
PrivateImmutable, PrivateMutable and PrivateSet got modified to directly contain the owner instead of implicitly "containing it" by including it in the storage slot via a Map.
These state variables now implement a newly introduced OwnedStateVariable trait (see docs of OwnedStateVariable for explanation of what it is).
These changes make the state variables incompatible with Map and now instead these should be wrapped in new Owned state variable:
#[storage]
struct Storage<Context> {
- private_nfts: Map<AztecAddress, PrivateSet<NFTNote, Context>, Context>,
+ private_nfts: Owned<PrivateSet<NFTNote, Context>, Context>,
}
Note that even though the types of your state variables are changing from Map<AztecAddress, T, Context> to Owned<T, Context>, usage remains unchanged:
let nft_notes = self.storage.private_nfts.at(from).pop_notes(NoteGetterOptions::new().select(NFTNote::properties().token_id, Comparator.EQ, token_id).set_limit(1));
With this change the underlying notes will inherit the storage slot of the Owned state variable.
This is unlike Map where the nested state variable got the storage slot computed as hash([map_storage_slot, key]).
if you had PrivateImmutable or PrivateMutable defined out of a Map, e.g.:
#[storage]
struct Storage<Context> {
signing_public_key: PrivateImmutable<PublicKeyNote, Context>,
}
you were most likely dealing with some kind of admin flow where only the admin can modify the state variable.
Now, unfortunately, there is a bit of a regression and you will need to wrap the state variable in Owned and call at on the state var:
+ use aztec::state_vars::Owned;
#[storage]
struct Storage<Context> {
- signing_public_key: PrivateImmutable<PublicKeyNote, Context>,
+ signing_public_key: Owned<PrivateImmutable<PublicKeyNote, Context>, Context>,
}
#[external("private")]
fn my_external_function() {
- self.storage.signing_public_key.initialize(pub_key_note)
+ self.storage.signing_public_key.at(self.address).initialize(pub_key_note)
.emit(self.address, MessageDelivery.CONSTRAINED_ONCHAIN);
}
We are likely to come up with a concept of admin state variables in the future.
None of the reference notes now contain the owner so if you manually construct AddressNote, UintNote or ValueNote you need to update the call to new method:
- let note = UintNote::new(156, owner);
+ let note = UintNote::new(156);
[Aztec.nr] Note randomness is now handled internally
In order to prevent pre-image attacks, it is necessary to inject randomness to notes. Aztec.nr users were previously expected to add said randomness to their custom note types. From now on, Aztec.nr takes care of handling randomness as built-in note metadata, making it impossible to miss for library users. This change breaks backwards compatibility as we'll discuss below.
Changes to Aztec.nr note types
If you're using any of the following note types, please be aware that randomness no longer is an explicit attribute in them.
- ValueNote
- UintNote
- NFTNote
- AddressNote
Migrating your custom note types: refer to UintNote as an example of how to migrate
We show the changes to UintNote below since it serves as a good example of the adjustments you will need to make to your own custom note types, including those that need to support partial notes.
Remove randomness from note struct
pub struct UintNote {
/// The owner of the note, i.e. the account whose nullifier secret key is required to compute the nullifier.
owner: AztecAddress,
- /// Random value, protects against note hash preimage attacks.
- randomness: Field,
/// The number stored in the note.
value: u128,
}
impl UintNote {
pub fn new(value: u128, owner: AztecAddress) -> Self {
- let randomness = unsafe { random() };
- Self { value, owner, randomness }
+ Self { value, owner }
}
Add randomness to compute_note_hash implementation
The NoteHash trait now requires compute_note_hash to receive a randomness field. This impacts
pub trait NoteHash {
/// ...
- fn compute_note_hash(self, storage_slot: Field) -> Field;
+ fn compute_note_hash(self, storage_slot: Field, randomness: Field) -> Field;
Then in trait implementations:
impl NoteHash for UintNote {
- fn compute_note_hash(self, storage_slot: Field) -> Field {
+ fn compute_note_hash(self, storage_slot: Field, randomness: Field) -> Field {
/// ...
- let private_content =
- UintPartialNotePrivateContent { owner: self.owner, randomness: self.randomness };
- let partial_note = PartialUintNote {
- commitment: private_content.compute_partial_commitment(storage_slot),
- };
+ let private_content =
+ UintPartialNotePrivateContent { owner: self.owner };
+ let partial_note = PartialUintNote {
+ commitment: private_content.compute_partial_commitment(storage_slot, randomness),
+ };
It's worth noting that this change also affects how partial notes are structured and handled.
pub fn partial(
owner: AztecAddress,
storage_slot: Field,
randomness: Field,
context: &mut PrivateContext,
recipient: AztecAddress,
completer: AztecAddress,
) -> PartialUintNote {
- let commitment = UintPartialNotePrivateContent { owner, randomness }
- .compute_partial_commitment(storage_slot);
+ let commitment = UintPartialNotePrivateContent { owner }
+ .compute_partial_commitment(storage_slot, randomness);
let private_log_content =
- UintPartialNotePrivateLogContent { owner, randomness, public_log_tag: commitment };
+ UintPartialNotePrivateLogContent { owner, public_log_tag: commitment };
let encrypted_log = note::compute_partial_note_private_content_log(
private_log_content,
storage_slot,
+ randomness,
recipient,
);
/// ...
}
struct UintPartialNotePrivateContent {
owner: AztecAddress,
- randomness: Field,
}
impl UintPartialNotePrivateContent {
- fn compute_partial_commitment(self, storage_slot: Field) -> Field {
+ fn compute_partial_commitment(self, storage_slot: Field, randomness: Field) -> Field {
poseidon2_hash_with_separator(
- self.pack().concat([storage_slot]),
+ self.pack().concat([storage_slot, randomness]),
DOM_SEP__NOTE_HASH,
)
}
}
struct UintPartialNotePrivateLogContent {
public_log_tag: Field,
owner: AztecAddress,
- randomness: Field,
}
Note size
As a result of this change, the maximum packed length of the content of a note is 11 fields, down from 12. This is a direct consequence of moving the randomness field from the note content structure to the note's metadata.
RetrievedNote now includes randomness field
pub struct RetrievedNote<Note> {
pub note: Note,
pub contract_address: AztecAddress,
+ pub randomness: Field,
pub metadata: NoteMetadata,
}
[L1 Contracts] Block is now Checkpoint
A checkpoint is now the primary unit handled by the L1 contracts.
A checkpoint may contain one or more L2 blocks. The protocol circuits already support producing multiple blocks per checkpoint. Updating the L1 contracts to operate on checkpoints allow L2 blockchain to advance faster.
Below are the API and event renames reflecting this change:
- event L2BlockProposed
+ event CheckpointProposed
- event BlockInvalidated
+ event CheckpointInvalidated
- function getEpochForBlock(uint256 _blockNumber) external view returns (Epoch);
+ function getEpochForCheckpoint(uint256 _checkpointNumber) external view returns (Epoch);
- function getProvenBlockNumber() external view returns (uint256);
+ function getProvenCheckpointNumber() external view returns (uint256);
- function getPendingBlockNumber() external view returns (uint256);
+ function getPendingCheckpointNumber() external view returns (uint256);
- function getBlock(uint256 _blockNumber) external view returns (BlockLog memory);
+ function getCheckpoint(uint256 _checkpointNumber) external view returns (CheckpointLog memory);
- function getBlockReward() external view returns (uint256);
+ function getCheckpointReward() external view returns (uint256);
Additionally, any function or struct that previously referenced an L2 block number now uses a checkpoint number instead:
- function status(uint256 _blockNumber) external view returns (
+ function status(uint256 _checkpointNumber) external view returns (
- uint256 provenBlockNumber,
+ uint256 provenCheckpointNumber,
bytes32 provenArchive,
- uint256 pendingBlockNumber,
+ uint256 pendingCheckpointNumber,
bytes32 pendingArchive,
bytes32 archiveOfMyBlock,
Epoch provenEpochNumber
);
Note: current node softwares still produce exactly one L2 block per checkpoint, so for now checkpoint numbers and L2 block numbers remain equal. This may change once multi-block checkpoints are enabled.
[Aztec.js] Wallet interface changes
simulateTx is now batchable
The simulateTx method on the Wallet interface is now batchable, meaning it can be called as part of a batch operation using wallet.batch(). This allows you to batch simulations together with other wallet operations like registerContract, sendTx, and registerSender.
- // Could not batch simulations
- const simulationResult = await wallet.simulateTx(executionPayload, options);
+ // Can now batch simulations with other operations
+ const results = await wallet.batch([
+ { name: 'registerContract', args: [instance, artifact] },
+ { name: 'simulateTx', args: [executionPayload, options] },
+ { name: 'sendTx', args: [anotherPayload, sendOptions] },
+ ]);
ExecutionPayload moved to @aztec/stdlib/tx
The ExecutionPayload type has been moved from @aztec/aztec.js to @aztec/stdlib/tx. Update your imports accordingly.
- import { ExecutionPayload } from '@aztec/aztec.js';
+ import { ExecutionPayload } from '@aztec/stdlib/tx';
+ // Or import from the re-export in aztec.js/tx:
+ import { ExecutionPayload } from '@aztec/aztec.js/tx';
ExecutionPayload now includes feePayer property
The ExecutionPayload class now includes an optional feePayer property that specifies which address is paying for the fee in the execution payload (if any)
const payload = new ExecutionPayload(
calls,
authWitnesses,
capsules,
extraHashedArgs,
+ feePayer // optional AztecAddress
);
This was previously provided as part of the SendOptions (and others) in the wallet interface, which could cause problems if a payload was assembled with a payment method and the parameter was later omitted. This means SendOptions now loses embeddedPaymentMethodFeePayer
-wallet.simulateTx(executionPayload, { from: address, embeddedFeePaymentMethodFeePayer: feePayer });
+wallet.simulateTx(executionPayload, { from: address });
simulateUtility signature and return type changed
The simulateUtility method signature has changed to accept a FunctionCall object instead of separate functionName, args, and to parameters. Additionally, the return type has changed from AbiDecoded to Fr[].
- const result: AbiDecoded = await wallet.simulateUtility(functionName, args, to, authWitnesses);
+ const result: UtilitySimulationResult = await wallet.simulateUtility(functionCall, authWitnesses?);
+ // result.result is now Fr[] instead of AbiDecoded
The new signature takes:
functionCall: AFunctionCallobject containingname,args,to,selector,type,isStatic,hideMsgSender, andreturnTypesauthWitnesses(optional): An array ofAuthWitnessobjects
The first argument is exactly the same as what goes into ExecutionPayload.calls. As such, the data is already encoded. The return value is now UtilitySimulationResult with result: Fr[] instead of returning an AbiDecoded value directly. You'll need to decode the Fr[] array yourself if you need typed results.
Contract.at() is now synchronous and no longer calls registerContract
The Contract.at() method (and generated contract .at() methods) is now synchronous and no longer automatically registers the contract with the wallet. This reduces unnecessary artifact storage and RPC calls.
- const contract = await TokenContract.at(address, wallet);
+ const contract = TokenContract.at(address, wallet);
Important: You now need to explicitly call registerContract if you want the wallet to store the contract instance and artifact. This is only necessary when:
- An app first registers a contract
- An app tries to update a contract's artifact
If you need to register the contract, do so explicitly:
// Get the instance from deployment
const { contract, instance } = await TokenContract.deploy(wallet, ...args)
.send({ from: address })
.wait();
// wallet already has it registered, since the deploy method does it by default
// to avoid it, set skipContractRegistration: true in the send options.
// Register it with another wallet
await otherWallet.registerContract(instance, TokenContract.artifact);
// Now you can use the contract
const otherContract = TokenContract.at(instance.address, otherWallet);
Publicly deployed contract instances can be retrieved via node.getContract(address). Otherwise and if deployment parameters are known, an instance can be computed via the getContractInstanceFromInstantiationParams from @aztec/aztec.js/contracts
registerContract signature simplified
The registerContract method now takes a ContractInstanceWithAddress instead of a Contract object, and the artifact parameter is now optional. If the artifact is not provided, the wallet will attempt to look it up from its contract class storage.
- await wallet.registerContract(contract);
+ await wallet.registerContract(instance, artifact?);
The method now only accepts:
instance: AContractInstanceWithAddressobjectartifact(optional): AContractArtifactobjectsecretKey(optional): A secret key for privacy keys registration
Return value of getNotes no longer contains a recipient and it contains some other additional info
Return value of getNotes used to be defined as Promise<UniqueNote[]> and now it's defined as Promise<UniqueNote[]>.
NoteDao is mostly a super-set of UniqueNote but it doesn't contain a recipient.
Having the recipient in the return value has been redundant as the same outcome can be achieved by populating the scopes array in NoteFilter with the recipient value.
Changes to getPrivateEvents
The signature of getPrivateEvents has changed for two reasons:
- To align it with how other query methods that include filtering by block range work (for example,
AztecNode#getPublicLogs) - To enrich the returned private events with metadata.
getPrivateEvents<T>(
- contractAddress: AztecAddress,
- eventMetadata: EventMetadataDefinition,
- from: number,
- numBlocks: number,
- recipients: AztecAddress[],
- ): Promise<T[]>;
+ eventFilter: PrivateEventFilter,
+ ): Promise<PrivateEvent<T>[]>;
PrivateEvent<T> bundles together an ABI decoded event of type T, with metadata of type InTx:
export type InBlock = {
l2BlockNumber: BlockNumber;
l2BlockHash: L2BlockHash;
};
export type InTx = InBlock & {
txHash: TxHash;
};
export type PrivateEvent<T> = {
event: T;
metadata: InTx;
};
You will need to update any calls to Wallet#getPrivateEvents accordingly. See below for before/after comparison which conserves
semantics.
Pay special attention to the fact that the old method expects a numBlocks parameter that instructs it to
return numBlocks blocks after fromBlock, whereas the new version expects an (exclusive) toBlock block number.
Also note we're replacing recipient terminology with scope. While underlying data types are equivalent (they are Aztec addresses), they have different semantics. Messages have a recipient who will be able to receive and process them. As a result of processing messages for a given recipient address, PXE might discover events. Those events are then said to be in scope for that address.
- const events = await context.client.getPrivateEvents(contractAddress, eventMetadata, 42, 10, [recipient]);
- doSomethingWithAnEvent(events[0]);
+ const events = await context.client.getPrivateEvents(eventMetadata, {
+ contractAddress,
+ fromBlock: BlockNumber(42),
+ toBlock: BlockNumber(42 + 10),
+ scopes: [scope],
+ });
+ doSomethingWithAnEvent(events[0].event);
Please refer to the wallet interface js-docs for further details.
[CLI] Command refactor
The sandbox command has been renamed and remapped to "local network". We believe this conveys better what is actually being spun up when running it.
REMOVED/RENAMED:
aztec start --sandbox: nowaztec start --local-network
[Aztec.nr] - Contract API redesign
In this release we decided to largely redesign our contract API. Most of the changes here are not a breaking change
(only renaming of original #[internal] to #[only_self] and storage now being available on the newly introduced
self struct are a breaking change).
1. Renaming of original #[internal] as #[only_self]
We want for internal to mean the same as in Solidity where internal function can be called only from the same contract
and is also inlined (EVM JUMP opcode and not EVM CALL). The original implementation of our #[internal] macro also
results in the function being callable only from the same contract but it results in a different call (hence it doesn't
map to EVM JUMP). This is very confusing for people that know Solidity hence we are doing the rename. A true
#[internal] will be introduced in the future.
To migrate your contracts simply rename all the occurrences of #[internal] with #[only_self] and update the imports:
- use aztec::macros::functions::internal;
+ use aztec::macros::functions::only_self;
#[external("public")]
- #[internal]
+ #[only_self]
fn _deduct_public_balance(owner: AztecAddress, amount: u64) {
...
}
2. Introducing of new #[internal]
Same as in Solidity internal functions are functions that are callable from inside the contract. Unlike #[only_self] functions, internal functions are inlined (e.g. akin to EVM's JUMP and not EVM's CALL).
Internal function can be called using the following API which leverages the new self struct (see change 3 below for
details):
self.internal.my_internal_function(...)
Private internal functions can only be called from other private external or internal functions. Public internal functions can only be called from other public external or internal functions.
3. Introducing self in contracts and a new call interface
Aztec contracts now automatically inject a self parameter into every contract function, providing a unified interface
for accessing the contract's address, storage, calling of function and an execution context.
What is self?
self is an instance of ContractSelf<Context, Storage> that provides:
self.address- The contract's own addressself.storage- Access to your contract's storageself.context- The execution context (private, public, or utility)self.msg_sender()- Get the address of the callerself.emit(...)- Emit eventsself.call(...)- Call an external functionself.view(...)- Call an external function staticallyself.enqueue(...)- Enqueue a call to an external functionself.enqueue_view(...)- Enqueue a call to an external functionself.enqueue_incognito(...)- Enqueue a call to an external function but hides themsg_senderself.enqueue_view_incognito(...)- Enqueue a static call to an external function but hides themsg_senderself.set_as_teardown(...)- Enqueue a call to an external public function and sets the call as teardownself.set_as_teardown_incognito(...)- Enqueue a call to an external public function and sets the call as teardown and hides themsg_senderself.internal.my_internal_fn(...)- Call an internal function
self also provides you with convenience API to call and enqueue calls to external functions from within the same
contract (this is just a convenience API as self.call(MyContract::at(self.address).my_external_fn(...)) would also
work):
self.call_self.my_external_fn(...)- Call external function from within the same contractself.enqueue_self.my_public_external_fn(...)self.call_self_static.my_static_external_fn(...)self.enqueue_self_static.my_static_external_public_fn(...)
How it works
The #[external(...)] macro automatically injects self into your function. When you write:
#[external("private")]
fn transfer(amount: u128, recipient: AztecAddress) {
let sender = self.msg_sender().unwrap();
self.storage.balances.at(sender).sub(amount);
self.storage.balances.at(recipient).add(amount);
}
The macro transforms it to initialize self with the context and storage before your code executes.
Migration guide
Before: Access context and storage as separate parameters
#[external("private")]
fn old_transfer(amount: u128, recipient: AztecAddress) {
let storage = Storage::init(context);
let sender = context.msg_sender().unwrap();
storage.balances.at(sender).sub(amount);
}
After: Use self to access everything
#[external("private")]
fn new_transfer(amount: u128, recipient: AztecAddress) {
let sender = self.msg_sender().unwrap();
self.storage.balances.at(sender).sub(amount);
}
Key changes
- Storage and context access:
Storage and context are no longer injected into the function as standalone variables and instead you need to access them via self:
- let balance = storage.balances.at(owner).read();
+ let balance = self.storage.balances.at(owner).read();
- context.push_nullifier(nullifier);
+ self.context.push_nullifier(nullifier);
Note that context is expected to be use only when needing to access a low-level API (like directly emitting a nullifier).
-
Getting caller address: Use
self.msg_sender()instead ofcontext.msg_sender()- let caller = context.msg_sender().unwrap();
+ let caller = self.msg_sender().unwrap(); -
Getting contract address: Use
self.addressinstead ofcontext.this_address()- let this_contract = context.this_address();
+ let this_contract = self.address; -
Emitting events:
In private functions:
- emit_event_in_private(event, context, recipient, delivery_mode);
+ self.emit(event, recipient, delivery_mode);In public functions:
- emit_event_in_public(event, context);
+ self.emit(event); -
Calling functions:
In private functions:
- Token::at(stable_coin).mint_to_public(to, amount).call(&mut context);
+ self.call(Token::at(stable_coin).mint_to_public(to, amount));
Example: Full contract migration
Before:
#[external("private")]
fn withdraw(amount: u128, recipient: AztecAddress) {
let storage = Storage::init(context);
let sender = context.msg_sender().unwrap();
let token = storage.donation_token.get_note().get_address();
// ... withdrawal logic
emit_event_in_private(Withdraw { withdrawer, amount }, context, withdrawer, MessageDelivery.UNCONSTRAINED_ONCHAIN);
}
After:
#[external("private")]
fn withdraw(amount: u128, recipient: AztecAddress) {
let sender = self.msg_sender().unwrap();
let token = self.storage.donation_token.get_note().get_address();
// ... withdrawal logic
self.emit(Withdraw { withdrawer, amount }, withdrawer, MessageDelivery.UNCONSTRAINED_ONCHAIN);
}
No-longer allowing calling of non-view function statically via the old higher-level API
We used to allow calling of non-view function statically as follows:
MyContract::at(address).my_non_view_function(...).view(context);
MyContract::at(address).my_non_view_function(...).enqueue_view(context);
This is no-longer allowed and if you will want to call a function statically you will need to mark the function with
#[view].
Phase checks
Now private external functions check by default that no phase change from non revertible to revertible happens during the execution of the function or any of its nested calls. If you're developing a function
that handles phase change (you call context.end_setup() or call a function that you expect will change phase) you need to opt out of the phase check using the #[nophasecheck] macro. Also, now it's possible to know if you're in the revertible phase of the transaction at any point using self.context.in_revertible_phase().
[aztec command] Moving functionality of aztec-nargo to aztec command
aztec-nargo has been deprecated and all workflows should now migrate to the aztec command that fully replaces aztec-nargo:
-
For contract initialization:
aztec init(Behaves like
nargo init, but defaults to a contract project.) -
For testing:
aztec test(Starts the Aztec TXE and runs your tests.)
-
For compiling contracts:
aztec compile(Transpiles your contracts and generates verification keys.)
3.0.0-devnet.4
[aztec.js] Removal of barrel export
aztec.js is now divided into granular exports, which improves loading performance in node.js and also makes the job of web bundlers easier:
-import { AztecAddress, Fr, getContractInstanceFromInstantiationParams, type Wallet } from '@aztec/aztec.js';
+import { AztecAddress } from '@aztec/aztec.js/addresses';
+import { getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts';
+import { Fr } from '@aztec/aztec.js/fields';
+import type { Wallet } from '@aztec/aztec.js/wallet';
Additionally, some general utilities reexported from foundation have been removed:
-export { toBigIntBE } from '@aztec/foundation/bigint-buffer';
-export { sha256, Grumpkin, Schnorr } from '@aztec/foundation/crypto';
-export { makeFetch } from '@aztec/foundation/json-rpc/client';
-export { retry, retryUntil } from '@aztec/foundation/retry';
-export { to2Fields, toBigInt } from '@aztec/foundation/serialize';
-export { sleep } from '@aztec/foundation/sleep';
-export { elapsed } from '@aztec/foundation/timer';
-export { type FieldsOf } from '@aztec/foundation/types';
-export { fileURLToPath } from '@aztec/foundation/url';
getSenders renamed to getAddressBook in wallet interface
An app could request "contacts" from the wallet, which don't necessarily have to be senders in the wallet's PXE. This method has been renamed to reflect that fact:
-wallet.getSenders();
+wallet.getAddressBook();
Removal of proveTx from Wallet interface
Exposing this method on the interface opened the door for certain types of attacks, were an app could route proven transactions through malicious nodes (that stored them for later decryption, or collected user IPs for example). It also made transactions difficult to track for the wallet, since they could be sent without their knowledge at any time. This change also affects ContractFunctionInteraction and DeployMethod, which no longer expose a prove() method.
msg_sender is now an Option<AztecAddress> type.
Because Aztec has native account abstraction, the very first function call of a tx has no msg_sender. (Recall, the first function call of an Aztec transaction is always a private function call).
Previously (before this change) we'd been silently setting this first msg_sender to be AztecAddress::from_field(-1);, and enforcing this value in the protocol's kernel circuits. Now we're passing explicitness to smart contract developers by wrapping msg_sender in an Option type. We'll explain the syntax shortly.
We've also added a new protocol feature. Previously (before this change) whenever a public function call was enqueued by a private function (a so-called private->public call), the called public function (and hence the whole world) would be able to see msg_sender. For some use cases, visibility of msg_sender is important, to ensure the caller executed certain checks in private-land. For #[only_self] public functions, visibility of msg_sender is unavoidable (the caller of an #[only_self] function must be the same contract address by definition). But for some use cases, a visible msg_sender is an unnecessary privacy leakage.
We therefore have added a feature where msg_sender can be optionally set to Option<AztecAddress>::none() for enqueued public function calls (aka private->public calls). We've been colloquially referring to this as "setting msg_sender to null".
Aztec.nr diffs
Note: we'll be doing another pass at this aztec.nr syntax in the near future.
Given the above, the syntax for accessing msg_sender in Aztec.nr is slightly different:
For most public and private functions, to adjust to this change, you can make this change to your code:
- let sender: AztecAddress = context.msg_sender();
+ let sender: AztecAddress = context.msg_sender().unwrap();
Recall that Option::unwrap() will throw if the Option is "none".
Indeed, most smart contract functions will require access to a proper contract address (instead of a "null" value), in order to do bookkeeping (allocation of state variables against user addresses), and so in such cases throwing is sensible behaviour.
If you want to output a useful error message when unwrapping fails, you can use Option::expect:
- let sender: AztecAddress = context.msg_sender();
+ let sender: AztecAddress = context.msg_sender().expect(f"Sender must not be none!");
For a minority of functions, a "null" msg_sender will be acceptable:
- A private entrypoint function.
- A public function which doesn't seek to do bookkeeping against
msg_sender.
Some apps might even want to assert that the msg_sender is "null" to force their users into strong privacy practices:
let sender: Option<AztecAddress> = context.msg_sender();
assert(sender.is_none());
Enqueueing public function calls
Auto-generated contract interfaces
When you use the #[aztec] macro, it will generate a noir contract interface for your contract, behind the scenes.
This provides pretty syntax when you come to call functions of that contract. E.g.:
Token::at(context.this_address())._increase_public_balance(to, amount).enqueue(&mut context);
In keeping with this new feature of being able to enqueue public function calls with a hidden msg_sender, there are some new methods that can be chained instead of .enqueue(...):
enqueue_incognito-- akin toenqueue, butmsg_senderis set "null".enqueue_view_incognito-- akin toenqueue_view, butmsg_senderis "null".set_as_teardown_incognito-- akin toset_as_teardown, butmsg_senderis "null".
The name "incognito" has been chosen to imply "msg_sender will not be visible to observers".
These new functions enable the calling contract to specify that it wants its address to not be visible to the called public function. This is worth re-iterating: it is the caller's choice. A smart contract developer who uses these functions must be sure that the target public function will accept a "null" msg_sender. It would not be good (for example) if the called public function did context.msg_sender().unwrap(), because then a public function that is called via enqueue_incognito would always fail! Hopefully smart contract developers will write sufficient tests to catch such problems during development!
Making lower-level public function calls from the private context
This is discouraged vs using the auto-generated contract interfaces described directly above.
If you do use any of these low-level methods of the PrivateContext in your contract:
call_public_functionstatic_call_public_functioncall_public_function_no_argsstatic_call_public_function_no_argscall_public_function_with_calldata_hashset_public_teardown_functionset_public_teardown_function_with_calldata_hash
... there is a new hide_msg_sender: bool parameter that you will need to specify.
Aztec.js diffs
Note: we'll be doing another pass at this aztec.js syntax in the near future.
When lining up a new tx, the FunctionCall struct has been extended to include a hide_msg_sender: bool field.
is_public & hide_msg_sender-- will make a public call withmsg_senderset to "null".is_public & !hide_msg_sender-- will make a public call with a visiblemsg_sender, as was the case before this new feature.!is_public & hide_msg_sender-- Incompatible flags.!is_public & !hide_msg_sender-- will make a private call with a visiblemsg_sender(noting that since it's a private function call, themsg_senderwill only be visible to the called private function, but not to the rest of the world).
[cli-wallet]
The deploy-account command now requires the address (or alias) of the account to deploy as an argument, not a parameter
+aztec-wallet deploy-account main
-aztec-wallet deploy-account -f main
This release includes a major architectural change to the system. The PXE JSON RPC Server has been removed, and PXE is now available only as a library to be used by wallets.
[Aztec node]
Network config. The node now pulls default configuration from the public repository AztecProtocol/networks after it applies the configuration it takes from the running environment and the configuration values baked into the source code. See associated Design document
[Aztec.js]
Removing Aztec cheatcodes
The Aztec cheatcodes class has been removed. Its functionality can be replaced by using the getNotes(...) function directly available on our TestWallet, along with the relevant functions available on the Aztec Node interface (note that the cheatcodes were generally just a thin wrapper around the Aztec Node interface).
CLI Wallet commands dropped from aztec command
The following commands used to be exposed by both the aztec and the aztec-wallet commands:
- import-test-accounts
- create-account
- deploy-account
- deploy
- send
- simulate
- profile
- bridge-fee-juice
- create-authwit
- authorize-action
- get-tx
- cancel-tx
- register-sender
- register-contract
These were dropped from aztec and now are exposed only by the cli-wallet command exposed by the @aztec/cli-wallet package.
PXE commands dropped from aztec command
The following commands were dropped from the aztec command:
add-contract: use can be replaced withregister-contracton ourcli-walletget-contract-data: debug-only and not considered important enough to need a replacementget-accounts: debug-only and can be replaced by loading aliases fromcli-walletget-account: debug-only and can be replaced by loading aliases fromcli-walletget-pxe-info: debug-only and not considered important enough to need a replacement
[Aztec.nr]
Replacing #[private], #[public], #[utility] with #[external(...)] macro
The original naming was not great in that it did not sufficiently communicate what the given macro did.
We decided to rename #[private] as #[external("private")], #[public] as #[external("public")], and #[utility] as #[external("utility")] to better communicate that these functions are externally callable and to specify their execution context. In this sense, external now means the exact same thing as in Solidity, i.e. a function that can be called from other contracts, and that can only be invoked via a contract call (i.e. the CALL opcode in the EVM, and a kernel call/AVM CALL opcode in Aztec).
You have to do the following changes in your contracts:
Update import:
- use aztec::macros::functions::private;
- use aztec::macros::functions::public;
- use aztec::macros::functions::utility;
+ use aztec::macros::functions::external;
Update attributes of your functions:
- #[private]
+ #[external("private")]
fn my_private_func() {
- #[public]
+ #[external("public")]
fn my_public_func() {
- #[utility]
+ #[external("utility")]
fn my_utility_func() {
Dropping remote mutable references to public context
PrivateContext generally needs to be passed as a mutable reference to functions because it does actually hold state
we're mutating. This is not the case for PublicContext, or UtilityContext - these are just marker objects that
indicate the current execution mode and make available the correct subset of the API. For this reason we have dropped
the mutable reference from the API.
If you've passed the context as an argument to custom functions you will need to do the following migration (example from our token contract):
#[contract_library_method]
fn _finalize_transfer_to_private(
from_and_completer: AztecAddress,
amount: u128,
partial_note: PartialUintNote,
- context: &mut PublicContext,
- storage: Storage<&mut PublicContext>,
+ context: PublicContext,
+ storage: Storage<PublicContext>,
) {
...
}
Authwit Test Helper now takes env
The add_private_authwit_from_call_interface test helper available in test::helpers::authwit now takes a TestEnvironment parameter, mirroring add_public_authwit_from_call_interface. This adds some unfortunate verbosity, but there are bigger plans to improve authwit usage in Noir tests in the near future.
add_private_authwit_from_call_interface(
+ env,
on_behalf_of,
caller,
call_interface,
);
Historical block renamed as anchor block
A historical block term has been used as a term that denotes the block against which a private part of a tx has been executed. This name is ambiguous and for this reason we've introduce "anchor block". This naming change resulted in quite a few changes and if you've access private context's or utility context's block header you will need to update your code:
- let header = context.get_block_header();
+ let header = context.get_anchor_block_header();
Removed ValueNote utils
The value_note::utils module has been removed because it was incorrect to have those in the value note package.
For the increment function you can easily just insert the note:
- use value_note::utils;
- utils::increment(storage.notes.at(owner), value, owner, sender);
+ let note = ValueNote::new(value, owner);
+ storage.notes.at(owner).insert(note).emit(&mut context, owner, MessageDelivery.CONSTRAINED_ONCHAIN);
PrivateMutable: replace / initialize_or_replace behaviour change
Motivation:
Updating a note used to require reading it first (via get_note, which nullifies and recreates it) and then calling replace — effectively proving a note twice. Now, replace accepts a callback that transforms the current note directly, and initialize_or_replace simply uses this updated replace internally. This reduces circuit cost while maintaining exactly one current note.
Key points:
replace(self, new_note)(old) →replace(self, f)(new), whereftakes the current note and returns a transformed note.initialize_or_replace(self, note)(old) →initialize_or_replace(self, f)(new), whereftakes anOptionwith the current none, ornoneif uninitialized.- Previous note is automatically nullified before the new note is inserted.
NoteEmission<Note>still requires.emit()or.discard().
Example Migration:
- let current_note = storage.my_var.get_note();
- let new_note = f(current_note);
- storage.my_var.replace(new_note);
+ storage.my_var.replace(|current_note| f(current_note));
- storage.my_var.initialize_or_replace(new_note);
+ storage.my_var.initialize_or_replace(|_| new_note);
This makes it easy and efficient to handle both initialization and current value mutation via initialize_or_replace, e.g. if implementing a note that simply counts how many times it has been read:
+ storage.my_var.initialize_or_replace(|opt_current: Option<Note>| opt_current.unwrap_or(0 /* initial value */) + 1);
- The callback can be a closure (inline) or a named function.
- Any previous assumptions that replace simply inserts a new_note directly must be updated.
Unified oracles into single get_utility_context oracle
The following oracles:
- get_contract_address,
- get_block_number,
- get_timestamp,
- get_chain_id,
- get_version
were replaced with a single get_utility_context oracle whose return value contains all the values returned from the removed oracles.
If you have used one of these removed oracles before, update the import, e.g.:
- aztec::oracle::execution::get_chain_id;
+ aztec::oracle::execution::get_utility_context
and get the value out of the returned utility context:
- let chain_id = get_chain_id();
+ let chain_id = get_utility_context().chain_id();
Note emission API changes
The note emission API has been significantly reworked to provide clearer semantics around message delivery guarantees. The key changes are:
encode_and_encrypt_notehas been removed in favor of callingemitdirectly withMessageDelivery.CONSTRAINED_ONCHAINencode_and_encrypt_note_unconstrainedhas been removed in favor of callingemitdirectly withMessageDelivery.UNCONSTRAINED_ONCHAINencode_and_encrypt_note_and_emit_as_offchain_messagehas been removed in favor of usingemitwithMessageDelivery.UNCONSTRAINED_OFFCHAIN- Note emission now takes a
delivery_modeparameter with the following values:CONSTRAINED_ONCHAIN: For onchain delivery with cryptographic guarantees that recipients can discover and decrypt messages. Uses constrained encryption but is slower to prove. Best for critical messages that contracts need to verify.UNCONSTRAINED_ONCHAIN: For onchain delivery without encryption constraints. Faster proving but trusts the sender. Good when the sender is incentivized to perform encryption correctly (e.g. they are buying something and will only get it if the recipient sees the note). No guarantees that recipients will be able to find or decrypt messages.UNCONSTRAINED_OFFCHAIN: For offchain delivery (e.g. cloud storage) without constraints. Lowest cost since no onchain storage needed. Requires custom infrastructure for delivery. No guarantees that messages will be delivered or that recipients will ever find them.
- The
contextobject no longer needs to be passed to these functions
Example migration:
First you need to update imports in your contract:
- aztec::messages::logs::note::encode_and_encrypt_note;
- aztec::messages::logs::note::encode_and_encrypt_note_unconstrained;
- aztec::messages::logs::note::encode_and_encrypt_note_and_emit_as_offchain_message;
+ aztec::messages::message_delivery::MessageDelivery;
Then update the emissions:
- storage.balances.at(from).sub(from, amount).emit(encode_and_encrypt_note(&mut context, from));
+ storage.balances.at(from).sub(from, amount).emit(&mut context, from, MessageDelivery.CONSTRAINED_ONCHAIN);
- storage.balances.at(from).add(from, change).emit(encode_and_encrypt_note_unconstrained(&mut context, from));
+ storage.balances.at(from).add(from, change).emit(&mut context, from, MessageDelivery.UNCONSTRAINED_ONCHAIN);
- storage.balances.at(owner).insert(note).emit(encode_and_encrypt_note_and_emit_as_offchain_message(&mut context, context.msg_sender());
+ storage.balances.at(owner).insert(note).emit(&mut context, context.msg_sender(), MessageDelivery.UNCONSTRAINED_OFFCHAIN);
2.0.2
[Public functions]
The L2 gas cost of the different AVM opcodes have been updated to reflect more realistic proving costs. Developers should review the L2 gas costs of executing public functions and reevaluate any hardcoded L2 gas limits.