Skip to main content

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.

0.62.0

[TXE] Single execution environment

Thanks to recent advancements in Brillig TXE performs every single call as if it was a nested call, spawning a new ACVM or AVM simulator without performance loss. This ensures every single test runs in a consistent environment and allows for clearer test syntax:

-let my_call_interface = MyContract::at(address).my_function(args);
-env.call_private(my_contract_interface)
+MyContract::at(address).my_function(args).call(&mut env.private());

This implies every contract has to be deployed before it can be tested (via env.deploy or env.deploy_self) and of course it has to be recompiled if its code was changed before TXE can use the modified bytecode.

Uniqueness of L1 to L2 messages

L1 to L2 messages have been updated to guarantee their uniqueness. This means that the hash of an L1 to L2 message cannot be precomputed, and must be obtained from the MessageSent event emitted by the Inbox contract, found in the L1 transaction receipt that inserted the message:

event MessageSent(uint256 indexed l2BlockNumber, uint256 index, bytes32 indexed hash);

This event now also includes an index. This index was previously required to consume an L1 to L2 message in a public function, and now it is also required for doing so in a private function, since it is part of the message hash preimage. The PrivateContext in aztec-nr has been updated to reflect this:

pub fn consume_l1_to_l2_message(
&mut self,
content: Field,
secret: Field,
sender: EthAddress,
+ leaf_index: Field,
) {

This change has also modified the internal structure of the archiver database, making it incompatible with previous ones. Last, the API for obtaining an L1 to L2 message membership witness has been simplified to leverage message uniqueness:

getL1ToL2MessageMembershipWitness(
blockNumber: L2BlockNumber,
l1ToL2Message: Fr,
- startIndex: bigint,
): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>] | undefined>;

Address is now a point

The address now serves as someone's public key to encrypt incoming notes. An address point has a corresponding address secret, which is used to decrypt the notes encrypted with the address point.

Notes no longer store a hash of the nullifier public keys, and now store addresses

Because of removing key rotation, we can now store addresses as the owner of a note. Because of this and the above change, we can and have removed the process of registering a recipient, because now we do not need any keys of the recipient.

example_note.nr

-npk_m_hash: Field
+owner: AztecAddress

PXE Interface

-registerRecipient(completeAddress: CompleteAddress)

0.58.0

[l1-contracts] Inbox's MessageSent event emits global tree index

Earlier MessageSent event in Inbox emitted a subtree index (index of the message in the subtree of the l2Block). But the nodes and Aztec.nr expects the index in the global L1_TO_L2_MESSAGES_TREE. So to make it easier to parse this, Inbox now emits this global index.

0.57.0

Changes to PXE API and `ContractFunctionInteraction``

PXE APIs have been refactored to better reflect the lifecycle of a Tx (execute private -> simulate kernels -> simulate public (estimate gas) -> prove -> send)

  • .simulateTx: Now returns a TxSimulationResult, containing the output of private execution, kernel simulation and public simulation (optional).
  • .proveTx: Now accepts the result of executing the private part of a transaction, so simulation doesn't have to happen again.

Thanks to this refactor, ContractFunctionInteraction has been updated to remove its internal cache and avoid bugs due to its mutable nature. As a result our type-safe interfaces now have to be used as follows:

-const action = MyContract.at(address).method(args);
-await action.prove();
-await action.send().wait();
+const action = MyContract.at(address).method(args);
+const provenTx = await action.prove();
+await provenTx.send().wait();

It's still possible to use .send() as before, which will perform proving under the hood.

More changes are coming to these APIs to better support gas estimation mechanisms and advanced features.

Changes to public calling convention

Contracts that include public functions (that is, marked with #[public]), are required to have a function public_dispatch(selector: Field) which acts as an entry point. This will be soon the only public function registered/deployed in contracts. The calling convention is updated so that external calls are made to this function.

If you are writing your contracts using Aztec-nr, there is nothing you need to change. The public_dispatch function is automatically generated by the #[aztec] macro.

[Aztec.nr] Renamed unsafe_rand to random

Since this is an unconstrained function, callers are already supposed to include an unsafe block, so this function has been renamed for reduced verbosity.

-use aztec::oracle::unsafe_rand::unsafe_rand;
+use aztec::oracle::random::random;

-let random_value = unsafe { unsafe_rand() };
+let random_value = unsafe { random() };

[Aztec.js] Removed L2Block.fromFields

L2Block.fromFields was a syntactic sugar which is causing issues so we've removed it.

-const l2Block = L2Block.fromFields({ header, archive, body });
+const l2Block = new L2Block(archive, header, body);

[Aztec.nr] Removed SharedMutablePrivateGetter

This state variable was deleted due to it being difficult to use safely.

[Aztec.nr] Changes to NullifiableNote

The compute_nullifier_without_context function is now unconstrained. It had always been meant to be called in unconstrained contexts (which is why it did not receive the context object), but now that Noir supports trait functions being unconstrained this can be implemented properly. Users must add the unconstrained keyword to their implementations of the trait:

impl NullifiableNote for MyCustomNote {
- fn compute_nullifier_without_context(self) -> Field {
+ unconstrained fn compute_nullifier_without_context(self) -> Field {

[Aztec.nr] Make TestEnvironment unconstrained

All of TestEnvironment's functions are now unconstrained, preventing accidentally calling them in a constrained circuit, among other kinds of user error. Becuase they work with mutable references, and these are not allowed to cross the constrained/unconstrained barrier, tests that use TestEnvironment must also become unconstrained. The recommended practice is to make all Noir tests and test helper functions be `unconstrained:

#[test]
-fn test_my_function() {
+unconstrained fn test_my_function() {
let env = TestEnvironment::new();

[Aztec.nr] removed encode_and_encrypt_note and renamed encode_and_encrypt_note_with_keys to encode_and_encrypt_note

contract XYZ {
- use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note_with_keys;
+ use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note;
...

- numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_note_with_keys(&mut context, owner_ovpk_m, owner_ivpk_m, owner));
+ numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_note(&mut context, owner_ovpk_m, owner_ivpk_m, owner));
}

0.56.0

[Aztec.nr] Changes to contract definition

We've migrated the Aztec macros to use the newly introduce meta programming Noir feature. Due to being Noir-based, the new macros are less obscure and can be more easily modified.

As part of this transition, some changes need to be applied to Aztec contracts:

  • The top level contract block needs to have the #[aztec] macro applied to it.
  • All #[aztec(name)] macros are renamed to #[name].
  • The storage struct (the one that gets the #[storage] macro applied) but be generic over a Context type, and all state variables receive this type as their last generic type parameter.
+ use dep::aztec::macros::aztec;

#[aztec]
contract Token {
+ use dep::aztec::macros::{storage::storage, events::event, functions::{initializer, private, view, public}};

- #[aztec(storage)]
- struct Storage {
+ #[storage]
+ struct Storage<Context> {
- admin: PublicMutable<AztecAddress>,
+ admin: PublicMutable<AztecAddress, Context>,
- minters: Map<AztecAddress, PublicMutable<bool>>,
+ minters: Map<AztecAddress, PublicMutable<bool, Context>, Context>,
}

- #[aztec(public)]
- #[aztec(initializer)]
+ #[public]
+ #[initializer]
fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) {
...
}

- #[aztec(public)]
- #[aztec(view)]
- fn public_get_name() -> FieldCompressedString {
+ #[public]
+ #[view]
fn public_get_name() -> FieldCompressedString {
...
}

[Aztec.nr] Changes to NoteInterface

The new macro model prevents partial trait auto-implementation: they either implement the entire trait or none of it. This means users can no longer implement part of NoteInterface and have the rest be auto-implemented.

For this reason we've separated the methods which are auto-implemented and those which needs to be implemented manually into two separate traits: the auto-implemented ones stay in the NoteInterface trace and the manually implemented ones were moved to NullifiableNote (name likely to change):

-#[aztec(note)]
+#[note]
struct AddressNote {
...
}

-impl NoteInterface<ADDRESS_NOTE_LEN, ADDRESS_NOTE_BYTES_LEN> for AddressNote {
+impl NullifiableNote for AddressNote {
fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field {
...
}

fn compute_nullifier_without_context(self) -> Field {
...
}
}

[Aztec.nr] Changes to contract interface

The Contract::storage() static method has been renamed to Contract::storage_layout().

-    let fee_payer_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, fee_payer);
- let user_balances_slot = derive_storage_slot_in_map(Token::storage().balances.slot, user);
+ let fee_payer_balances_slot = derive_storage_slot_in_map(Token::storage_layout().balances.slot, fee_payer);
+ let user_balances_slot = derive_storage_slot_in_map(Token::storage_layout().balances.slot, user);

Key rotation removed

The ability to rotate incoming, outgoing, nullifying and tagging keys has been removed - this feature was easy to misuse and not worth the complexity and gate count cost. As part of this, the Key Registry contract has also been deleted. The API for fetching public keys has been adjusted accordingly:

- let keys = get_current_public_keys(&mut context, account);
+ let keys = get_public_keys(account);

[Aztec.nr] Rework NoteGetterOptions::select

The select function in both NoteGetterOptions and NoteViewerOptions no longer takes an Option of a comparator, but instead requires an explicit comparator to be passed. Additionally, the order of the parameters has been changed so that they are (lhs, operator, rhs). These two changes should make invocations of the function easier to read:

- options.select(ValueNote::properties().value, amount, Option::none())
+ options.select(ValueNote::properties().value, Comparator.EQ, amount)

0.53.0

[Aztec.nr] Remove OwnedNote and create UintNote

OwnedNote allowed having a U128 value in the custom note while ValueNote restricted to just a Field.

We have removed OwnedNote but are introducing a more genric UintNote within aztec.nr

#[aztec(note)]
struct UintNote {
// The integer stored by the note
value: U128,
// The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent.
npk_m_hash: Field,
// Randomness of the note to hide its contents
randomness: Field,
}

[TXE] logging

You can now use debug_log() within your contract to print logs when using the TXE

Remember to set the following environment variables to activate debug logging:

export DEBUG="aztec:*"
export LOG_LEVEL="debug"

[Account] no assert in is_valid_impl

is_valid_impl method in account contract asserted if signature was true. Instead now we will return the verification to give flexibility to developers to handle it as they please.

- let verification = std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message);
- assert(verification == true);
- true
+ std::ecdsa_secp256k1::verify_signature(public_key.x, public_key.y, signature, hashed_message)

0.49.0

Key Rotation API overhaul

Public keys (ivpk, ovpk, npk, tpk) should no longer be fetched using the old get_[x]pk_m methods on the Header struct, but rather by calling get_current_public_keys, which returns a PublicKeys struct with all four keys at once:

+use dep::aztec::keys::getters::get_current_public_keys;

-let header = context.header();
-let owner_ivpk_m = header.get_ivpk_m(&mut context, owner);
-let owner_ovpk_m = header.get_ovpk_m(&mut context, owner);
+let owner_keys = get_current_public_keys(&mut context, owner);
+let owner_ivpk_m = owner_keys.ivpk_m;
+let owner_ovpk_m = owner_keys.ovpk_m;

If using more than one key per account, this will result in very large circuit gate count reductions.

Additionally, get_historical_public_keys was added to support reading historical keys using a historical header:

+use dep::aztec::keys::getters::get_historical_public_keys;

let historical_header = context.header_at(some_block_number);
-let owner_ivpk_m = header.get_ivpk_m(&mut context, owner);
-let owner_ovpk_m = header.get_ovpk_m(&mut context, owner);
+let owner_keys = get_historical_public_keys(historical_header, owner);
+let owner_ivpk_m = owner_keys.ivpk_m;
+let owner_ovpk_m = owner_keys.ovpk_m;

0.48.0

NoteInterface changes

compute_note_hash_and_nullifier* functions were renamed as compute_nullifier* and the compute_nullifier function now takes note_hash_for_nullify as an argument (this allowed us to reduce gate counts and the hash was typically computed before). Also compute_note_hash_for_consumption function was renamed as compute_note_hash_for_nullify.

impl NoteInterface<VALUE_NOTE_LEN, VALUE_NOTE_BYTES_LEN> for ValueNote {
- fn compute_note_hash_and_nullifier(self, context: &mut PrivateContext) -> (Field, Field) {
- let note_hash_for_nullify = compute_note_hash_for_consumption(self);
- let secret = context.request_nsk_app(self.npk_m_hash);
- let nullifier = poseidon2_hash_with_separator([
- note_hash_for_nullify,
- secret,
- ],
- GENERATOR_INDEX__NOTE_NULLIFIER as Field,
- );
- (note_hash_for_nullify, nullifier)
- }
- fn compute_note_hash_and_nullifier_without_context(self) -> (Field, Field) {
- let note_hash_for_nullify = compute_note_hash_for_consumption(self);
- let secret = get_nsk_app(self.npk_m_hash);
- let nullifier = poseidon2_hash_with_separator([
- note_hash_for_nullify,
- secret,
- ],
- GENERATOR_INDEX__NOTE_NULLIFIER as Field,
- );
- (note_hash_for_nullify, nullifier)
- }

+ fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field {
+ let secret = context.request_nsk_app(self.npk_m_hash);
+ poseidon2_hash_with_separator([
+ note_hash_for_nullify,
+ secret
+ ],
+ GENERATOR_INDEX__NOTE_NULLIFIER as Field,
+ )
+ }
+ fn compute_nullifier_without_context(self) -> Field {
+ let note_hash_for_nullify = compute_note_hash_for_nullify(self);
+ let secret = get_nsk_app(self.npk_m_hash);
+ poseidon2_hash_with_separator([
+ note_hash_for_nullify,
+ secret,
+ ],
+ GENERATOR_INDEX__NOTE_NULLIFIER as Field,
+ )
+ }
}

Fee Juice rename

The name of the canonical Gas contract has changed to Fee Juice. Update noir code:

-GasToken::at(contract_address)
+FeeJuice::at(contract_address)

Additionally, NativePaymentMethod and NativePaymentMethodWithClaim have been renamed to FeeJuicePaymentMethod and FeeJuicePaymentMethodWithClaim.

PrivateSet::pop_notes(...)

The most common flow when working with notes is obtaining them from a PrivateSet via get_notes(...) and then removing them via PrivateSet::remove(...). This is cumbersome and it results in unnecessary constraints due to a redundant note read request checks in the remove function.

For this reason we've implemented pop_notes(...) which gets the notes, removes them from the set and returns them. This tight coupling of getting notes and removing them allowed us to safely remove the redundant read request check.

Token contract diff:

-let options = NoteGetterOptions::with_filter(filter_notes_min_sum, target_amount).set_limit(max_notes);
-let notes = self.map.at(owner).get_notes(options);
-let mut subtracted = U128::from_integer(0);
-for i in 0..options.limit {
- if i < notes.len() {
- let note = notes.get_unchecked(i);
- self.map.at(owner).remove(note);
- subtracted = subtracted + note.get_amount();
- }
-}
-assert(minuend >= subtrahend, "Balance too low");
+let options = NoteGetterOptions::with_filter(filter_notes_min_sum, target_amount).set_limit(max_notes);
+let notes = self.map.at(owner).pop_notes(options);
+let mut subtracted = U128::from_integer(0);
+for i in 0..options.limit {
+ if i < notes.len() {
+ let note = notes.get_unchecked(i);
+ subtracted = subtracted + note.get_amount();
+ }
+}
+assert(minuend >= subtrahend, "Balance too low");

Note that pop_notes may not have obtained and removed any notes! The caller must place checks on the returned notes, e.g. in the example above by checking a sum of balances, or by checking the number of returned notes (assert_eq(notes.len(), expected_num_notes)).

0.47.0

[Aztec sandbox] TXE deployment changes

The way simulated deployments are done in TXE tests has changed to avoid relying on TS interfaces. It is now possible to do it by directly pointing to a Noir standalone contract or workspace:

-let deployer = env.deploy("path_to_contract_ts_interface");
+let deployer = env.deploy("path_to_contract_root_folder_where_nargo_toml_is", "ContractName");

Extended syntax for more use cases:

// The contract we're testing
env.deploy_self("ContractName"); // We have to provide ContractName since nargo it's ready to support multi-contract files

// A contract in a workspace
env.deploy("../path/to/workspace@package_name", "ContractName"); // This format allows locating the artifact in the root workspace target folder, regardless of internal code organization

The deploy function returns a Deployer, which requires performing a subsequent call to without_initializer(), with_private_initializer() or with_public_initializer() just like before in order to actually deploy the contract.

[CLI] Command refactor and unification + aztec test

Sandbox commands have been cleaned up and simplified. Doing aztec-up now gets you the following top-level commands:

aztec: All the previous commands + all the CLI ones without having to prefix them with cli. Run aztec for help! aztec-nargo: No changes

REMOVED/RENAMED:

  • aztec-sandbox and aztec sandbox: now aztec start --sandbox
  • aztec-builder: now aztec codegen and aztec update

ADDED:

  • aztec test [options]: runs aztec start --txe && aztec-nargo test --oracle-resolver http://aztec:8081 --silence-warnings [options] via docker-compose allowing users to easily run contract tests using TXE

0.45.0

[Aztec.nr] Remove unencrypted logs from private

They leak privacy so is a footgun!

0.44.0

[Aztec.nr] Autogenerate Serialize methods for events

#[aztec(event)]
struct WithdrawalProcessed {
who: Field,
amount: Field,
}

-impl Serialize<2> for WithdrawalProcessed {
- fn serialize(self: Self) -> [Field; 2] {
- [self.who.to_field(), self.amount as Field]
- }
}

[Aztec.nr] rename encode_and_encrypt_with_keys to encode_and_encrypt_note_with_keys

contract XYZ {
- use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_with_keys;
+ use dep::aztec::encrypted_logs::encrypted_note_emission::encode_and_encrypt_note_with_keys;
....

- numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_with_keys(&mut context, owner_ovpk_m, owner_ivpk_m));
+ numbers.at(owner).initialize(&mut new_number).emit(encode_and_encrypt_note_with_keys(&mut context, owner_ovpk_m, owner_ivpk_m));
}

[Aztec.nr] changes to NoteInterface

compute_nullifier function was renamed to compute_note_hash_and_nullifier and now the function has to return not only the nullifier but also the note hash used to compute the nullifier. The same change was done to compute_nullifier_without_context function. These changes were done because having the note hash exposed allowed us to not having to re-compute it again in destroy_note function of Aztec.nr which led to significant decrease in gate counts (see the optimization PR for more details).

- impl NoteInterface<VALUE_NOTE_LEN, VALUE_NOTE_BYTES_LEN> for ValueNote {
- fn compute_nullifier(self, context: &mut PrivateContext) -> Field {
- let note_hash_for_nullify = compute_note_hash_for_consumption(self);
- let secret = context.request_nsk_app(self.npk_m_hash);
- poseidon2_hash([
- note_hash_for_nullify,
- secret,
- GENERATOR_INDEX__NOTE_NULLIFIER as Field,
- ])
- }
-
- fn compute_nullifier_without_context(self) -> Field {
- let note_hash_for_nullify = compute_note_hash_for_consumption(self);
- let secret = get_nsk_app(self.npk_m_hash);
- poseidon2_hash([
- note_hash_for_nullify,
- secret,
- GENERATOR_INDEX__NOTE_NULLIFIER as Field,
- ])
- }
- }
+ impl NoteInterface<VALUE_NOTE_LEN, VALUE_NOTE_BYTES_LEN> for ValueNote {
+ fn compute_note_hash_and_nullifier(self, context: &mut PrivateContext) -> (Field, Field) {
+ let note_hash_for_nullify = compute_note_hash_for_consumption(self);
+ let secret = context.request_nsk_app(self.npk_m_hash);
+ let nullifier = poseidon2_hash([
+ note_hash_for_nullify,
+ secret,
+ GENERATOR_INDEX__NOTE_NULLIFIER as Field,
+ ]);
+ (note_hash_for_nullify, nullifier)
+ }
+
+ fn compute_note_hash_and_nullifier_without_context(self) -> (Field, Field) {
+ let note_hash_for_nullify = compute_note_hash_for_consumption(self);
+ let secret = get_nsk_app(self.npk_m_hash);
+ let nullifier = poseidon2_hash([
+ note_hash_for_nullify,
+ secret,
+ GENERATOR_INDEX__NOTE_NULLIFIER as Field,
+ ]);
+ (note_hash_for_nullify, nullifier)
+ }
+ }

[Aztec.nr] note_getter returns BoundedVec

The get_notes and view_notes function no longer return an array of options (i.e. [Option<Note>, N_NOTES]) but instead a BoundedVec<Note, N_NOTES>. This better conveys the useful property the old array had of having all notes collapsed at the beginning of the array, which allows for powerful optimizations and gate count reduction when setting the options.limit value.

A BoundedVec has a max_len(), which equals the number of elements it can hold, and a len(), which equals the number of elements it currently holds. Since len() is typically not knwon at compile time, iterating over a BoundedVec looks slightly different than iterating over an array of options:

- let option_notes = get_notes(options);
- for i in 0..option_notes.len() {
- if option_notes[i].is_some() {
- let note = option_notes[i].unwrap_unchecked();
- }
- }
+ let notes = get_notes(options);
+ for i in 0..notes.max_len() {
+ if i < notes.len() {
+ let note = notes.get_unchecked(i);
+ }
+ }

To further reduce gate count, you can iterate over options.limit instead of max_len(), since options.limit is guaranteed to be larger or equal to len(), and smaller or equal to max_len():

- for i in 0..notes.max_len() {
+ for i in 0..options.limit {

[Aztec.nr] static private authwit

The private authwit validation is now making a static call to the account contract instead of passing over control flow. This is to ensure that it cannot be used for re-entry.

To make this change however, we cannot allow emitting a nullifier from the account contract, since that would break the static call. Instead, we will be changing the spend_private_authwit to a verify_private_authwit and in the auth library emit the nullifier. This means that the "calling" contract will now be emitting the nullifier, and not the account. For example, for a token contract, the nullifier is now emitted by the token contract. However, as this is done inside the auth library, the token contract doesn't need to change much.

The biggest difference is related to "cancelling" an authwit. Since it is no longer in the account contract, you cannot just emit a nullifier from it anymore. Instead it must rely on the token contract providing functionality for cancelling.

There are also a few general changes to how authwits are generated, namely to more easily support the data required for a validity lookup now. Previously we could lookup the message_hash directly at the account contract, now we instead need to use the inner_hash and the contract of the consumer to figure out if it have already been emitted.

A minor extension have been made to the authwit creations to make it easier to sign a specific a hash with a specific caller, e.g., the inner_hash can be provided as {consumer, inner_hash} to the createAuthWit where it previously needed to do a couple of manual steps to compute the outer hash. The computeOuterAuthWitHash have been amde internal and the computeAuthWitMessageHash can instead be used to compute the values similarly to other authwit computations.

const innerHash = computeInnerAuthWitHash([Fr.ZERO, functionSelector.toField(), entrypointPackedArgs.hash]);
-const outerHash = computeOuterAuthWitHash(
- this.dappEntrypointAddress,
- new Fr(this.chainId),
- new Fr(this.version),
- innerHash,
-);
+const messageHash = computeAuthWitMessageHash(
+ { consumer: this.dappEntrypointAddress, innerHash },
+ { chainId: new Fr(this.chainId), version: new Fr(this.version) },
+);

If the wallet is used to compute the authwit, it will populate the chain id and version instead of requiring it to be provided by tha actor.

const innerHash = computeInnerAuthWitHash([Fr.fromString('0xdead')]);
-const outerHash = computeOuterAuthWitHash(wallets[1].getAddress(), chainId, version, innerHash);
-const witness = await wallets[0].createAuthWit(outerHash);
+ const witness = await wallets[0].createAuthWit({ comsumer: accounts[1].address, inner_hash });

0.43.0

[Aztec.nr] break token.transfer() into transfer and transferFrom

Earlier we had just one function - transfer() which used authwits to handle the case where a contract/user wants to transfer funds on behalf of another user. To reduce circuit sizes and proof times, we are breaking up transfer and introducing a dedicated transferFrom() function like in the ERC20 standard.

[Aztec.nr] options.limit has to be constant

The limit parameter in NoteGetterOptions and NoteViewerOptions is now required to be a compile-time constant. This allows performing loops over this value, which leads to reduced circuit gate counts when setting a limit value.

[Aztec.nr] canonical public authwit registry

The public authwits are moved into a shared registry (auth registry) to make it easier for sequencers to approve for their non-revertible (setup phase) whitelist. Previously, it was possible to DOS a sequencer by having a very expensive authwit validation that fails at the end, now the whitelist simply need the registry.

Notable, this means that consuming a public authwit will no longer emit a nullifier in the account contract but instead update STORAGE in the public domain. This means that there is a larger difference between private and public again. However, it also means that if contracts need to approve, and use the approval in the same tx, it is transient and don't need to go to DA (saving 96 bytes).

For the typescript wallets this is handled so the APIs don't change, but account contracts should get rid of their current setup with approved_actions.

- let actions = AccountActions::init(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
+ let actions = AccountActions::init(&mut context, is_valid_impl);

For contracts we have added a set_authorized function in the auth library that can be used to set values in the registry.

- storage.approved_action.at(message_hash).write(true);
+ set_authorized(&mut context, message_hash, true);

[Aztec.nr] emit encrypted logs

Emitting or broadcasting encrypted notes are no longer done as part of the note creation, but must explicitly be either emitted or discarded instead.

+ use dep::aztec::encrypted_logs::encrypted_note_emission::{encode_and_encrypt, encode_and_encrypt_with_keys};

- storage.balances.sub(from, amount);
+ storage.balances.sub(from, amount).emit(encode_and_encrypt_with_keys(&mut context, from, from));
+ storage.balances.sub(from, amount).emit(encode_and_encrypt_with_keys(&mut context, from_ovpk, from_ivpk));
+ storage.balances.sub(from, amount).discard();

0.42.0

[Aztec.nr] Unconstrained Context

Top-level unconstrained execution is now marked by the new UnconstrainedContext, which provides access to the block number and contract address being used in the simulation. Any custom state variables that provided unconstrained functions should update their specialization parameter:

+ use dep::aztec::context::UnconstrainedContext;

- impl MyStateVariable<()> {
+ impl MyStateVariable<UnconstrainedContext> {

[Aztec.nr] Filtering is now constrained

The filter argument of NoteGetterOptions (typically passed via the with_filter() function) is now applied in a constraining environment, meaning any assertions made during the filtering are guaranteed to hold. This mirrors the behavior of the select() function.

[Aztec.nr] Emitting encrypted notes and logs

The emit_encrypted_log context function is now encrypt_and_emit_log or encrypt_and_emit_note.

- context.emit_encrypted_log(log1);
+ context.encrypt_and_emit_log(log1);
+ context.encrypt_and_emit_note(note1);

Broadcasting a note will call encrypt_and_emit_note in the background. To broadcast a generic event, use encrypt_and_emit_log with the same encryption parameters as notes require. Currently, only fields and arrays of fields are supported as events.

By default, logs emitted via encrypt_and_emit_log will be siloed with a masked contract address. To force the contract address to be revealed, so everyone can check it rather than just the log recipient, provide randomness = 0.

Public execution migrated to the Aztec Virtual Machine

What does this mean for me?

It should be mostly transparent, with a few caveats:

  • Not all Noir blackbox functions are supported by the AVM. Only Sha256, PedersenHash, Poseidon2Permutation, Keccak256, and ToRadix are supported.
  • For public functions, context.nullifier_exists(...) will now also consider pending nullifiers.
  • The following methods of PublicContext are not supported anymore: fee_recipient, fee_per_da_gas, fee_per_l2_gas, call_public_function_no_args, static_call_public_function_no_args, delegate_call_public_function_no_args, call_public_function_with_packed_args, set_return_hash, finish. However, in terms of functionality, the new context's interface should be equivalent (unless otherwise specified in this list).
  • Delegate calls are not yet supported in the AVM.
  • If you have types with custom serialization that you use across external contracts calls, you might need to modify its serialization to match how Noir would serialize it. This is a known problem unrelated to the AVM, but triggered more often when using it.
  • A few error messages might change format, so you might need to change your test assertions.

Internal details

Before this change, public bytecode was executed using the same simulator as in private: the ACIR simulator (and internally, the Brillig VM). On the Aztec.nr side, public functions accessed the context through PublicContext.

After this change, public bytecode will be run using the AVM simulator (the simulator for our upcoming zkVM). This bytecode is generated from Noir contracts in two steps: First, nargo compile produces an artifact which has Brillig bytecode for public functions, just as it did before. Second: the avm-transpiler takes that artifact, and it transpiles Brillig bytecode to AVM bytecode. This final artifact can now be deployed and used with the new public runtime.

On the Aztec.nr side, public functions keep accessing the context using PublicContext but the underlying implementation is switch with what formerly was the AvmContext.

0.41.0

[Aztec.nr] State variable rework

Aztec.nr state variables have been reworked so that calling private functions in public and vice versa is detected as an error during compilation instead of at runtime. This affects users in a number of ways:

New compile time errors

It used to be that calling a state variable method only available in public from a private function resulted in obscure runtime errors in the form of a failed _is_some assertion.

Incorrect usage of the state variable methods now results in compile time errors. For example, given the following function:

#[aztec(public)]
fn get_decimals() -> pub u8 {
storage.decimals.read_private()
}

The compiler will now error out with

Expected type SharedImmutable<_, &mut PrivateContext>, found type SharedImmutable<u8, &mut PublicContext>

The key component is the second generic parameter: the compiler expects a PrivateContext (becuse read_private is only available during private execution), but a PublicContext is being used instead (because of the #[aztec(public)] attribute).

Generic parameters in Storage

The Storage struct (the one marked with #[aztec(storage)]) should now be generic over a Context type, which matches the new generic parameter of all Aztec.nr libraries. This parameter is always the last generic parameter.

This means that, without any additional features, we'd end up with some extra boilerplate when declaring this struct:

#[aztec(storage)]
- struct Storage {
+ struct Storage<Context> {
- nonce_for_burn_approval: PublicMutable<Field>,
+ nonce_for_burn_approval: PublicMutable<Field, Context>,
- portal_address: SharedImmutable<EthAddress>,
+ portal_address: SharedImmutable<EthAddress, Context>,
- approved_action: Map<Field, PublicMutable<bool>>,
+ approved_action: Map<Field, PublicMutable<bool, Context>, Context>,
}

Because of this, the #[aztec(storage)] macro has been updated to automatically inject this Context generic parameter. The storage declaration does not require any changes.

Removal of Context

The Context type no longer exists. End users typically didn't use it, but if imported it needs to be deleted.

[Aztec.nr] View functions and interface navigation

It is now possible to explicitly state a function doesn't perform any state alterations (including storage, logs, nullifiers and/or messages from L2 to L1) with the #[aztec(view)] attribute, similarly to solidity's view function modifier.

    #[aztec(public)]
+ #[aztec(view)]
fn get_price(asset_id: Field) -> Asset {
storage.assets.at(asset_id).read()
}

View functions only generate a StaticCallInterface that doesn't include .call or .enqueue methods. Also, the denomination static has been completely removed from the interfaces, in favor of the more familiar view

- let price = PriceFeed::at(asset.oracle).get_price(0).static_call(&mut context).price;
+ let price = PriceFeed::at(asset.oracle).get_price(0).view(&mut context).price;
#[aztec(private)]
fn enqueue_public_get_value_from_child(target_contract: AztecAddress, value: Field) {
- StaticChild::at(target_contract).pub_get_value(value).static_enqueue(&mut context);
+ StaticChild::at(target_contract).pub_get_value(value).enqueue_view(&mut context);
}

Additionally, the Noir LSP will now honor "go to definitions" requests for contract interfaces (Ctrl+click), taking the user to the original function implementation.

[Aztec.js] Simulate changes

  • .simulate() now tracks closer the process performed by .send().wait(), specifically going through the account contract entrypoint instead of directly calling the intended function.
  • wallet.viewTx(...) has been renamed to wallet.simulateUnconstrained(...) to better clarify what it does.

[Aztec.nr] Keys: Token note now stores an owner master nullifying public key hash instead of an owner address

i.e.

struct TokenNote {
amount: U128,
- owner: AztecAddress,
+ npk_m_hash: Field,
randomness: Field,
}

Creating a token note and adding it to storage now looks like this:

- let mut note = ValueNote::new(new_value, owner);
- storage.a_private_value.insert(&mut note, true);
+ let owner_npk_m_hash = get_npk_m_hash(&mut context, owner);
+ let owner_ivpk_m = get_ivpk_m(&mut context, owner);
+ let mut note = ValueNote::new(new_value, owner_npk_m_hash);
+ storage.a_private_value.insert(&mut note, true, owner_ivpk_m);

Computing the nullifier similarly changes to use this master nullifying public key hash.

0.40.0

[Aztec.nr] Debug logging

The function debug_log_array_with_prefix has been removed. Use debug_log_format with {} instead. The special sequence {} will be replaced with the whole array. You can also use {0}, {1}, ... as usual with debug_log_format.

- debug_log_array_with_prefix("Prefix", my_array);
+ debug_log_format("Prefix {}", my_array);

0.39.0

[Aztec.nr] Mutable delays in SharedMutable

The type signature for SharedMutable changed from SharedMutable<T, DELAY> to SharedMutable<T, INITIAL_DELAY>. The behavior is the same as before, except the delay can now be changed after deployment by calling schedule_delay_change.

[Aztec.nr] get_public_key oracle replaced with get_ivpk_m

When implementing changes according to a new key scheme we had to change oracles. What used to be called encryption public key is now master incoming viewing public key.

- use dep::aztec::oracles::get_public_key::get_public_key;
+ use dep::aztec::keys::getters::get_ivpk_m;

- let encryption_pub_key = get_public_key(self.owner);
+ let ivpk_m = get_ivpk_m(context, self.owner);

0.38.0

[Aztec.nr] Emitting encrypted logs

The emit_encrypted_log function is now a context method.

- use dep::aztec::log::emit_encrypted_log;
- use dep::aztec::logs::emit_encrypted_log;

- emit_encrypted_log(context, log1);
+ context.emit_encrypted_log(log1);

0.36.0

FieldNote removed

FieldNote only existed for testing purposes, and was not a note type that should be used in any real application. Its name unfortunately led users to think that it was a note type suitable to store a Field value, which it wasn't.

If using FieldNote, you most likely want to use ValueNote instead, which has both randomness for privacy and an owner for proper nullification.

SlowUpdatesTree replaced for SharedMutable

The old SlowUpdatesTree contract and libraries have been removed from the codebase, use the new SharedMutable library instead. This will require that you add a global variable specifying a delay in blocks for updates, and replace the slow updates tree state variable with SharedMutable variables.

+ global CHANGE_ROLES_DELAY_BLOCKS = 5;

struct Storage {
- slow_update: SharedImmutable<AztecAddress>,
+ roles: Map<AztecAddress, SharedMutable<UserFlags, CHANGE_ROLES_DELAY_BLOCKS>>,
}

Reading from SharedMutable is much simpler, all that's required is to call get_current_value_in_public or get_current_value_in_private, depending on the domain.

- let caller_roles = UserFlags::new(U128::from_integer(slow.read_at_pub(context.msg_sender().to_field()).call(&mut context)));
+ let caller_roles = storage.roles.at(context.msg_sender()).get_current_value_in_public();

Finally, you can remove all capsule usage on the client code or tests, since those are no longer required when working with SharedMutable.

[Aztec.nr & js] Portal addresses

Deployments have been modified. No longer are portal addresses treated as a special class, being immutably set on creation of a contract. They are no longer passed in differently compared to the other variables and instead should be implemented using usual storage by those who require it. One should use the storage that matches the usecase - likely shared storage to support private and public.

This means that you will likely add the portal as a constructor argument

- fn constructor(token: AztecAddress) {
- storage.token.write(token);
- }
+ struct Storage {
...
+ portal_address: SharedImmutable<AztecAddress>,
+ }
+ fn constructor(token: AztecAddress, portal_address: EthAddress) {
+ storage.token.write(token);
+ storage.portal_address.initialize(portal_address);
+ }

And read it from storage whenever needed instead of from the context.

- context.this_portal_address(),
+ storage.portal_address.read_public(),

[Aztec.nr] Oracles

Oracle get_nullifier_secret_key was renamed to get_app_nullifier_secret_key and request_nullifier_secret_key function on PrivateContext was renamed as request_app_nullifier_secret_key.

- let secret = get_nullifier_secret_key(self.owner);
+ let secret = get_app_nullifier_secret_key(self.owner);
- let secret = context.request_nullifier_secret_key(self.owner);
+ let secret = context.request_app_nullifier_secret_key(self.owner);

[Aztec.nr] Contract interfaces

It is now possible to import contracts on another contracts and use their automatic interfaces to perform calls. The interfaces have the same name as the contract, and are automatically exported. Parameters are automatically serialized (using the Serialize<N> trait) and return values are automatically deserialized (using the Deserialize<N> trait). Serialize and Deserialize methods have to conform to the standard ACVM serialization schema for the interface to work!

  1. Only fixed length types are supported
  2. All numeric types become Fields
  3. Strings become arrays of Fields, one per char
  4. Arrays become arrays of Fields following rules 2 and 3
  5. Structs become arrays of Fields, with every item defined in the same order as they are in Noir code, following rules 2, 3, 4 and 5 (recursive)
- context.call_public_function(
- storage.gas_token_address.read_private(),
- FunctionSelector::from_signature("pay_fee(Field)"),
- [42]
- );
-
- context.call_public_function(
- storage.gas_token_address.read_private(),
- FunctionSelector::from_signature("pay_fee(Field)"),
- [42]
- );
-
- let _ = context.call_private_function(
- storage.subscription_token_address.read_private(),
- FunctionSelector::from_signature("transfer((Field),(Field),Field,Field)"),
- [
- context.msg_sender().to_field(),
- storage.subscription_recipient_address.read_private().to_field(),
- storage.subscription_price.read_private(),
- nonce
- ]
- );
+ use dep::gas_token::GasToken;
+ use dep::token::Token;
+
+ ...
+ // Public call from public land
+ GasToken::at(storage.gas_token_address.read_private()).pay_fee(42).call(&mut context);
+ // Public call from private land
+ GasToken::at(storage.gas_token_address.read_private()).pay_fee(42).enqueue(&mut context);
+ // Private call from private land
+ Token::at(asset).transfer(context.msg_sender(), storage.subscription_recipient_address.read_private(), amount, nonce).call(&mut context);

It is also possible to use these automatic interfaces from the local contract, and thus enqueue public calls from private without having to rely on low level context calls.

[Aztec.nr] Rename max block number setter

The request_max_block_number function has been renamed to set_tx_max_block_number to better reflect that it is not a getter, and that the setting is transaction-wide.

- context.request_max_block_number(value);
+ context.set_tx_max_block_number(value);

[Aztec.nr] Get portal address

The get_portal_address oracle was removed. If you need to get the portal address of SomeContract, add the following methods to it

#[aztec(private)]
fn get_portal_address() -> EthAddress {
context.this_portal_address()
}

#[aztec(public)]
fn get_portal_address_public() -> EthAddress {
context.this_portal_address()
}

and change the call to get_portal_address

- let portal_address = get_portal_address(contract_address);
+ let portal_address = SomeContract::at(contract_address).get_portal_address().call(&mut context);

[Aztec.nr] Required gas limits for public-to-public calls

When calling a public function from another public function using the call_public_function method, you must now specify how much gas you're allocating to the nested call. This will later allow you to limit the amount of gas consumed by the nested call, and handle any out of gas errors.

Note that gas limits are not yet enforced. For now, it is suggested you use dep::aztec::context::gas::GasOpts::default() which will forward all available gas.

+ use dep::aztec::context::gas::GasOpts;

- context.call_public_function(target_contract, target_selector, args);
+ context.call_public_function(target_contract, target_selector, args, GasOpts::default());

Note that this is not required when enqueuing a public function from a private one, since top-level enqueued public functions will always consume all gas available for the transaction, as it is not possible to handle any out-of-gas errors.

[Aztec.nr] Emitting unencrypted logs

The emit_unencrypted_logs function is now a context method.

- use dep::aztec::log::emit_unencrypted_log;
- use dep::aztec::log::emit_unencrypted_log_from_private;

- emit_unencrypted_log(context, log1);
- emit_unencrypted_log_from_private(context, log2);
+ context.emit_unencrypted_log(log1);
+ context.emit_unencrypted_log(log2);

0.33

[Aztec.nr] Storage struct annotation

The storage struct now identified by the annotation #[aztec(storage)], instead of having to rely on it being called Storage.

- struct Storage {
- ...
- }
+ #[aztec(storage)]
+ struct MyStorageStruct {
+ ...
+ }

[Aztec.js] Storage layout and note info

Storage layout and note information are now exposed in the TS contract artifact

- const note = new Note([new Fr(mintAmount), secretHash]);
- const pendingShieldStorageSlot = new Fr(5n); // storage slot for pending_shields
- const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // note type id for TransparentNote
- const extendedNote = new ExtendedNote(
- note,
- admin.address,
- token.address,
- pendingShieldStorageSlot,
- noteTypeId,
- receipt.txHash,
- );
- await pxe.addNote(extendedNote);
+ const note = new Note([new Fr(mintAmount), secretHash]);
+ const extendedNote = new ExtendedNote(
+ note,
+ admin.address,
+ token.address,
+ TokenContract.storage.pending_shields.slot,
+ TokenContract.notes.TransparentNote.id,
+ receipt.txHash,
+ );
+ await pxe.addNote(extendedNote);

[Aztec.nr] rand oracle is now called unsafe_rand

oracle::rand::rand has been renamed to oracle::unsafe_rand::unsafe_rand. This change was made to communicate that we do not constrain the value in circuit and instead we just trust our PXE.

- let random_value = rand();
+ let random_value = unsafe_rand();

[AztecJS] Simulate and get return values for ANY call and introducing prove()

Historically it have been possible to "view" unconstrained functions to simulate them and get the return values, but not for public nor private functions. This has lead to a lot of bad code where we have the same function implemented thrice, once in private, once in public and once in unconstrained. It is not possible to call simulate on any call to get the return values! However, beware that it currently always returns a Field array of size 4 for private and public. This will change to become similar to the return values of the unconstrained functions with proper return types.

-    #[aztec(private)]
- fn get_shared_immutable_constrained_private() -> pub Leader {
- storage.shared_immutable.read_private()
- }
-
- unconstrained fn get_shared_immutable() -> pub Leader {
- storage.shared_immutable.read_public()
- }

+ #[aztec(private)]
+ fn get_shared_immutable_private() -> pub Leader {
+ storage.shared_immutable.read_private()
+ }

- const returnValues = await contract.methods.get_shared_immutable().view();
+ const returnValues = await contract.methods.get_shared_immutable_private().simulate();
await expect(
- asset.withWallet(wallets[1]).methods.update_admin(newAdminAddress).simulate()).rejects.toThrow(
+ asset.withWallet(wallets[1]).methods.update_admin(newAdminAddress).prove()).rejects.toThrow(
"Assertion failed: caller is not admin 'caller_roles.is_admin'",
);

0.31.0

[Aztec.nr] Public storage historical read API improvement

history::public_value_inclusion::prove_public_value_inclusion has been renamed to history::public_storage::public_storage_historical_read, and its API changed slightly. Instead of receiving a value parameter it now returns the historical value stored at that slot.

If you were using an oracle to get the value to pass to prove_public_value_inclusion, drop the oracle and use the return value from public_storage_historical_read instead:

- let value = read_storage();
- prove_public_value_inclusion(value, storage_slot, contract_address, context);
+ let value = public_storage_historical_read(storage_slot, contract_address, context);

If you were proving historical existence of a value you got via some other constrained means, perform an assertion against the return value of public_storage_historical_read instead:

- prove_public_value_inclusion(value, storage_slot, contract_address, context);
+ assert(public_storage_historical_read(storage_slot, contract_address, context) == value);

0.30.0

[AztecJS] Simplify authwit syntax

- const messageHash = computeAuthWitMessageHash(accounts[1].address, action.request());
- await wallets[0].setPublicAuth(messageHash, true).send().wait();
+ await wallets[0].setPublicAuthWit({ caller: accounts[1].address, action }, true).send().wait();
const action = asset
.withWallet(wallets[1])
.methods.unshield(accounts[0].address, accounts[1].address, amount, nonce);
-const messageHash = computeAuthWitMessageHash(accounts[1].address, action.request());
-const witness = await wallets[0].createAuthWitness(messageHash);
+const witness = await wallets[0].createAuthWit({ caller: accounts[1].address, action });
await wallets[1].addAuthWitness(witness);

Also note some of the naming changes: setPublicAuth -> setPublicAuthWit createAuthWitness -> createAuthWit

[Aztec.nr] Automatic NoteInterface implementation and selector changes

Implementing a note required a fair amount of boilerplate code, which has been substituted by the #[aztec(note)] attribute.

+ #[aztec(note)]
struct AddressNote {
address: AztecAddress,
owner: AztecAddress,
randomness: Field,
header: NoteHeader
}

impl NoteInterface<ADDRESS_NOTE_LEN> for AddressNote {
- fn serialize_content(self) -> [Field; ADDRESS_NOTE_LEN]{
- [self.address.to_field(), self.owner.to_field(), self.randomness]
- }
-
- fn deserialize_content(serialized_note: [Field; ADDRESS_NOTE_LEN]) -> Self {
- AddressNote {
- address: AztecAddress::from_field(serialized_note[0]),
- owner: AztecAddress::from_field(serialized_note[1]),
- randomness: serialized_note[2],
- header: NoteHeader::empty(),
- }
- }
-
- fn compute_note_content_hash(self) -> Field {
- pedersen_hash(self.serialize_content(), 0)
- }
-
fn compute_nullifier(self, context: &mut PrivateContext) -> Field {
let note_hash_for_nullify = compute_note_hash_for_consumption(self);
let secret = context.request_nullifier_secret_key(self.owner);
pedersen_hash([
note_hash_for_nullify,
secret.low,
secret.high,
],0)
}

fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_consumption(self);
let secret = get_nullifier_secret_key(self.owner);
pedersen_hash([
note_hash_for_nullify,
secret.low,
secret.high,
],0)
}

- fn set_header(&mut self, header: NoteHeader) {
- self.header = header;
- }
-
- fn get_header(note: Self) -> NoteHeader {
- note.header
- }

fn broadcast(self, context: &mut PrivateContext, slot: Field) {
let encryption_pub_key = get_public_key(self.owner);
emit_encrypted_log(
context,
(*context).this_address(),
slot,
Self::get_note_type_id(),
encryption_pub_key,
self.serialize_content(),
);
}

- fn get_note_type_id() -> Field {
- 6510010011410111511578111116101
- }
}

Automatic note (de)serialization implementation also means it is now easier to filter notes using NoteGetterOptions.select via the ::properties() helper:

Before:

let options = NoteGetterOptions::new().select(0, amount, Option::none()).select(1, owner.to_field(), Option::none()).set_limit(1);

After:

let options = NoteGetterOptions::new().select(ValueNote::properties().value, amount, Option::none()).select(ValueNote::properties().owner, owner.to_field(), Option::none()).set_limit(1);

The helper returns a metadata struct that looks like this (if autogenerated)

ValueNoteProperties {
value: PropertySelector { index: 0, offset: 0, length: 32 },
owner: PropertySelector { index: 1, offset: 0, length: 32 },
randomness: PropertySelector { index: 2, offset: 0, length: 32 },
}

It can also be used for the .sort method.

0.27.0

initializer macro replaces constructor

Before this version, every contract was required to have exactly one constructor private function, that was used for deployment. We have now removed this requirement, and made constructor a function like any other.

To signal that a function can be used to initialize a contract, you must now decorate it with the #[aztec(initializer)] attribute. Initializers are regular functions that set an "initialized" flag (a nullifier) for the contract. A contract can only be initialized once, and contract functions can only be called after the contract has been initialized, much like a constructor. However, if a contract defines no initializers, it can be called at any time. Additionally, you can define as many initializer functions in a contract as you want, both private and public.

To migrate from current code, simply add an initializer attribute to your constructor functions.

+ #[aztec(initializer)]
#[aztec(private)]
fn constructor() { ... }

If your private constructor was used to just call a public internal initializer, then remove the private constructor and flag the public function as initializer. And if your private constructor was an empty one, just remove it.

0.25.0

[Aztec.nr] Static calls

It is now possible to perform static calls from both public and private functions. Static calls forbid any modification to the state, including L2->L1 messages or log generation. Once a static context is set through a static all, every subsequent call will also be treated as static via context propagation.

context.static_call_private_function(targetContractAddress, targetSelector, args);

context.static_call_public_function(targetContractAddress, targetSelector, args);

[Aztec.nr] Introduction to prelude

A new prelude module to include common Aztec modules and types. This simplifies dependency syntax. For example:

use dep::aztec::protocol_types::address::AztecAddress;
use dep::aztec::{
context::{PrivateContext, Context}, note::{note_header::NoteHeader, utils as note_utils},
state_vars::Map
};

Becomes:

use dep::aztec::prelude::{AztecAddress, NoteHeader, PrivateContext, Map};
use dep::aztec::context::Context;
use dep::aztec::notes::utils as note_utils;

This will be further simplified in future versions (See 4496 for further details).

The prelude consists of

prelude
pub use crate::{
context::{PackedReturns, PrivateContext, PublicContext},
note::{
note_getter_options::NoteGetterOptions,
note_header::NoteHeader,
note_interface::{NoteInterface, NullifiableNote},
note_viewer_options::NoteViewerOptions,
utils::compute_note_hash_and_optionally_a_nullifier as utils_compute_note_hash_and_optionally_a_nullifier,
},
state_vars::{
map::Map, private_immutable::PrivateImmutable, private_mutable::PrivateMutable,
private_set::PrivateSet, public_immutable::PublicImmutable, public_mutable::PublicMutable,
shared_immutable::SharedImmutable, shared_mutable::SharedMutable, storage::Storable,
},
};
pub use dep::protocol_types::{
abis::function_selector::FunctionSelector,
address::{AztecAddress, EthAddress},
point::Point,
traits::{Deserialize, Serialize},
};
Source code: noir-projects/aztec-nr/aztec/src/prelude.nr#L1-L23

internal is now a macro

The internal keyword is now removed from Noir, and is replaced by an aztec(internal) attribute in the function. The resulting behavior is exactly the same: these functions will only be callable from within the same contract.

Before:

#[aztec(private)]
internal fn double(input: Field) -> Field {
input * 2
}

After:

#[aztec(private)]
#[aztec(internal)]
fn double(input: Field) -> Field {
input * 2
}

[Aztec.nr] No SafeU120 anymore!

Noir now have overflow checks by default. So we don't need SafeU120 like libraries anymore.

You can replace it with U128 instead

Before:

SafeU120::new(0)

Now:

U128::from_integer(0)

[Aztec.nr] compute_note_hash_and_nullifier is now autogenerated

Historically developers have been required to include a compute_note_hash_and_nullifier function in each of their contracts. This function is now automatically generated, and all instances of it in contract code can be safely removed.

It is possible to provide a user-defined implementation, in which case auto-generation will be skipped (though there are no known use cases for this).

[Aztec.nr] Updated naming of state variable wrappers

We have decided to change the naming of our state variable wrappers because the naming was not clear. The changes are as follows:

  1. Singleton -> PrivateMutable
  2. ImmutableSingleton -> PrivateImmutable
  3. StablePublicState -> SharedImmutable
  4. PublicState -> PublicMutable

This is the meaning of "private", "public" and "shared": Private: read (R) and write (W) from private, not accessible from public Public: not accessible from private, R/W from public Shared: R from private, R/W from public

Note: SlowUpdates will be renamed to SharedMutable once the implementation is ready.

[Aztec.nr] Authwit updates

Authentication Witnesses have been updates such that they are now cancellable and scoped to a specific consumer. This means that the authwit nullifier must be emitted from the account contract, which require changes to the interface. Namely, the assert_current_call_valid_authwit_public and assert_current_call_valid_authwit in auth.nr will NO LONGER emit a nullifier. Instead it will call a spend_*_authwit function in the account contract - which will emit the nullifier and perform a few checks. This means that the is_valid functions have been removed to not confuse it for a non-mutating function (static). Furthermore, the caller parameter of the "authwits" have been moved "further out" such that the account contract can use it in validation, allowing scoped approvals from the account POV. For most contracts, this won't be changing much, but for the account contract, it will require a few changes.

Before:

#[aztec(public)]
fn is_valid_public(message_hash: Field) -> Field {
let actions = AccountActions::public(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.is_valid_public(message_hash)
}

#[aztec(private)]
fn is_valid(message_hash: Field) -> Field {
let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.is_valid(message_hash)
}

After:

#[aztec(private)]
fn verify_private_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::private(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.verify_private_authwit(inner_hash)
}

#[aztec(public)]
fn spend_public_authwit(inner_hash: Field) -> Field {
let actions = AccountActions::public(&mut context, ACCOUNT_ACTIONS_STORAGE_SLOT, is_valid_impl);
actions.spend_public_authwit(inner_hash)
}

0.24.0

Introduce Note Type IDs

Note Type IDs are a new feature which enable contracts to have multiple Maps with different underlying note types, something that was not possible before. This is done almost without any user intervention, though some minor changes are required.

The mandatory compute_note_hash_and_nullifier now has a fifth parameter note_type_id. Use this instead of storage_slot to determine which deserialization function to use.

Before:

unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
preimage: [Field; TOKEN_NOTE_LEN]
) -> pub [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);

if (storage_slot == storage.pending_shields.get_storage_slot()) {
note_utils::compute_note_hash_and_nullifier(TransparentNote::deserialize_content, note_header, preimage)
} else if (note_type_id == storage.slow_update.get_storage_slot()) {
note_utils::compute_note_hash_and_nullifier(FieldNote::deserialize_content, note_header, preimage)
} else {
note_utils::compute_note_hash_and_nullifier(TokenNote::deserialize_content, note_header, preimage)
}

Now:

unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
note_type_id: Field,
preimage: [Field; TOKEN_NOTE_LEN]
) -> pub [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);

if (note_type_id == TransparentNote::get_note_type_id()) {
note_utils::compute_note_hash_and_nullifier(TransparentNote::deserialize_content, note_header, preimage)
} else if (note_type_id == FieldNote::get_note_type_id()) {
note_utils::compute_note_hash_and_nullifier(FieldNote::deserialize_content, note_header, preimage)
} else {
note_utils::compute_note_hash_and_nullifier(TokenNote::deserialize_content, note_header, preimage)
}

The NoteInterface trait now has an additional get_note_type_id() function. This implementation will be autogenerated in the future, but for now providing any unique ID will suffice. The suggested way to do it is by running the Python command shown in the comment below:

impl NoteInterface<N> for MyCustomNote {
fn get_note_type_id() -> Field {
// python -c "print(int(''.join(str(ord(c)) for c in 'MyCustomNote')))"
771216711711511611110978111116101
}
}

[js] Importing contracts in JS

@aztec/noir-contracts is now @aztec/noir-contracts.js. You'll need to update your package.json & imports.

Before:

import { TokenContract } from "@aztec/noir-contracts/Token";

Now:

import { TokenContract } from "@aztec/noir-contracts.js/Token";

[Aztec.nr] Aztec.nr contracts location change in Nargo.toml

Aztec contracts are now moved outside of the yarn-project folder and into noir-projects, so you need to update your imports.

Before:

easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.23.0", directory = "yarn-project/noir-contracts/contracts/easy_private_token_contract"}

Now, update the yarn-project folder for noir-projects:

easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.24.0", directory = "noir-projects/noir-contracts/contracts/easy_private_token_contract"}

0.22.0

Note::compute_note_hash renamed to Note::compute_note_content_hash

The compute_note_hash function in of the Note trait has been renamed to compute_note_content_hash to avoid being confused with the actual note hash.

Before:

impl NoteInterface for CardNote {
fn compute_note_hash(self) -> Field {
pedersen_hash([
self.owner.to_field(),
], 0)
}

Now:

impl NoteInterface for CardNote {
fn compute_note_content_hash(self) -> Field {
pedersen_hash([
self.owner.to_field(),
], 0)
}

Introduce compute_note_hash_for_consumption and compute_note_hash_for_insertion

Makes a split in logic for note hash computation for consumption and insertion. This is to avoid confusion between the two, and to make it clear that the note hash for consumption is different from the note hash for insertion (sometimes).

compute_note_hash_for_consumption replaces compute_note_hash_for_read_or_nullify. compute_note_hash_for_insertion is new, and mainly used in `lifecycle.nr``

Note::serialize_content and Note::deserialize_content added to `NoteInterface

The NoteInterface have been extended to include serialize_content and deserialize_content functions. This is to convey the difference between serializing the full note, and just the content. This change allows you to also add a serialize function to support passing in a complete note to a function.

Before:

impl Serialize<ADDRESS_NOTE_LEN> for AddressNote {
fn serialize(self) -> [Field; ADDRESS_NOTE_LEN]{
[self.address.to_field(), self.owner.to_field(), self.randomness]
}
}
impl Deserialize<ADDRESS_NOTE_LEN> for AddressNote {
fn deserialize(serialized_note: [Field; ADDRESS_NOTE_LEN]) -> Self {
AddressNote {
address: AztecAddress::from_field(serialized_note[0]),
owner: AztecAddress::from_field(serialized_note[1]),
randomness: serialized_note[2],
header: NoteHeader::empty(),
}
}

Now

impl NoteInterface<ADDRESS_NOTE_LEN>  for AddressNote {
fn serialize_content(self) -> [Field; ADDRESS_NOTE_LEN]{
[self.address.to_field(), self.owner.to_field(), self.randomness]
}

fn deserialize_content(serialized_note: [Field; ADDRESS_NOTE_LEN]) -> Self {
AddressNote {
address: AztecAddress::from_field(serialized_note[0]),
owner: AztecAddress::from_field(serialized_note[1]),
randomness: serialized_note[2],
header: NoteHeader::empty(),
}
}
...
}

[Aztec.nr] No storage.init() and Serialize, Deserialize, NoteInterface as Traits, removal of SerializationMethods and SERIALIZED_LEN

Storage definition and initialization has been simplified. Previously:

struct Storage {
leader: PublicState<Leader, LEADER_SERIALIZED_LEN>,
legendary_card: Singleton<CardNote, CARD_NOTE_LEN>,
profiles: Map<AztecAddress, Singleton<CardNote, CARD_NOTE_LEN>>,
test: Set<CardNote, CARD_NOTE_LEN>,
imm_singleton: PrivateImmutable<CardNote, CARD_NOTE_LEN>,
}

impl Storage {
fn init(context: Context) -> Self {
Storage {
leader: PublicMutable::new(
context,
1,
LeaderSerializationMethods,
),
legendary_card: PrivateMutable::new(context, 2, CardNoteMethods),
profiles: Map::new(
context,
3,
|context, slot| {
PrivateMutable::new(context, slot, CardNoteMethods)
},
),
test: Set::new(context, 4, CardNoteMethods),
imm_singleton: PrivateImmutable::new(context, 4, CardNoteMethods),
}
}
}

Now:

struct Storage {
leader: PublicMutable<Leader>,
legendary_card: Singleton<CardNote>,
profiles: Map<AztecAddress, Singleton<CardNote>>,
test: Set<CardNote>,
imm_singleton: PrivateImmutable<CardNote>,
}

For this to work, Notes must implement Serialize, Deserialize and NoteInterface Traits. Previously:

use dep::aztec::protocol_types::address::AztecAddress;
use dep::aztec::{
note::{
note_header::NoteHeader,
note_interface::NoteInterface,
utils::compute_note_hash_for_read_or_nullify,
},
oracle::{
nullifier_key::get_nullifier_secret_key,
get_public_key::get_public_key,
},
log::emit_encrypted_log,
hash::pedersen_hash,
context::PrivateContext,
};

// Shows how to create a custom note

global CARD_NOTE_LEN: Field = 1;

impl CardNote {
pub fn new(owner: AztecAddress) -> Self {
CardNote {
owner,
}
}

pub fn serialize(self) -> [Field; CARD_NOTE_LEN] {
[self.owner.to_field()]
}

pub fn deserialize(serialized_note: [Field; CARD_NOTE_LEN]) -> Self {
CardNote {
owner: AztecAddress::from_field(serialized_note[1]),
}
}

pub fn compute_note_hash(self) -> Field {
pedersen_hash([
self.owner.to_field(),
],0)
}

pub fn compute_nullifier(self, context: &mut PrivateContext) -> Field {
let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(CardNoteMethods, self);
let secret = context.request_nullifier_secret_key(self.owner);
pedersen_hash([
note_hash_for_nullify,
secret.high,
secret.low,
],0)
}

pub fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(CardNoteMethods, self);
let secret = get_nullifier_secret_key(self.owner);
pedersen_hash([
note_hash_for_nullify,
secret.high,
secret.low,
],0)
}

pub fn set_header(&mut self, header: NoteHeader) {
self.header = header;
}

// Broadcasts the note as an encrypted log on L1.
pub fn broadcast(self, context: &mut PrivateContext, slot: Field) {
let encryption_pub_key = get_public_key(self.owner);
emit_encrypted_log(
context,
(*context).this_address(),
slot,
encryption_pub_key,
self.serialize(),
);
}
}

fn deserialize(serialized_note: [Field; CARD_NOTE_LEN]) -> CardNote {
CardNote::deserialize(serialized_note)
}

fn serialize(note: CardNote) -> [Field; CARD_NOTE_LEN] {
note.serialize()
}

fn compute_note_hash(note: CardNote) -> Field {
note.compute_note_hash()
}

fn compute_nullifier(note: CardNote, context: &mut PrivateContext) -> Field {
note.compute_nullifier(context)
}

fn compute_nullifier_without_context(note: CardNote) -> Field {
note.compute_nullifier_without_context()
}

fn get_header(note: CardNote) -> NoteHeader {
note.header
}

fn set_header(note: &mut CardNote, header: NoteHeader) {
note.set_header(header)
}

// Broadcasts the note as an encrypted log on L1.
fn broadcast(context: &mut PrivateContext, slot: Field, note: CardNote) {
note.broadcast(context, slot);
}

global CardNoteMethods = NoteInterface {
deserialize,
serialize,
compute_note_hash,
compute_nullifier,
compute_nullifier_without_context,
get_header,
set_header,
broadcast,
};

Now:

use dep::aztec::{
note::{
note_header::NoteHeader,
note_interface::NoteInterface,
utils::compute_note_hash_for_read_or_nullify,
},
oracle::{
nullifier_key::get_nullifier_secret_key,
get_public_key::get_public_key,
},
log::emit_encrypted_log,
hash::pedersen_hash,
context::PrivateContext,
protocol_types::{
address::AztecAddress,
traits::{Serialize, Deserialize, Empty}
}
};

// Shows how to create a custom note

global CARD_NOTE_LEN: Field = 1;

impl CardNote {
pub fn new(owner: AztecAddress) -> Self {
CardNote {
owner,
}
}
}

impl NoteInterface for CardNote {
fn compute_note_content_hash(self) -> Field {
pedersen_hash([
self.owner.to_field(),
],0)
}

fn compute_nullifier(self, context: &mut PrivateContext) -> Field {
let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(self);
let secret = context.request_nullifier_secret_key(self.owner);
pedersen_hash([
note_hash_for_nullify,
secret.high,
secret.low,
],0)
}

fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_read_or_nullify(self);
let secret = get_nullifier_secret_key(self.owner);
pedersen_hash([
note_hash_for_nullify,
secret.high,
secret.low,
],0)
}

fn set_header(&mut self, header: NoteHeader) {
self.header = header;
}

fn get_header(note: CardNote) -> NoteHeader {
note.header
}

fn serialize_content(self) -> [Field; CARD_NOTE_LEN]{
[self.owner.to_field()]
}

fn deserialize_content(serialized_note: [Field; CARD_NOTE_LEN]) -> Self {
AddressNote {
owner: AztecAddress::from_field(serialized_note[0]),
header: NoteHeader::empty(),
}
}

// Broadcasts the note as an encrypted log on L1.
fn broadcast(self, context: &mut PrivateContext, slot: Field) {
let encryption_pub_key = get_public_key(self.owner);
emit_encrypted_log(
context,
(*context).this_address(),
slot,
encryption_pub_key,
self.serialize(),
);
}
}

Public state must implement Serialize and Deserialize traits.

It is still possible to manually implement the storage initialization (for custom storage wrappers or internal types that don't implement the required traits). For the above example, the impl Storage section would look like this:

impl Storage {
fn init(context: Context) -> Self {
Storage {
leader: PublicMutable::new(
context,
1
),
legendary_card: PrivateMutable::new(context, 2),
profiles: Map::new(
context,
3,
|context, slot| {
PrivateMutable::new(context, slot)
},
),
test: Set::new(context, 4),
imm_singleton: PrivateImmutable::new(context, 4),
}
}
}

0.20.0

[Aztec.nr] Changes to NoteInterface

  1. Changing compute_nullifier() to compute_nullifier(private_context: PrivateContext)

    This API is invoked for nullifier generation within private functions. When using a secret key for nullifier creation, retrieve it through:

    private_context.request_nullifier_secret_key(account_address)

    The private context will generate a request for the kernel circuit to validate that the secret key does belong to the account.

    Before:

     pub fn compute_nullifier(self) -> Field {
    let secret = oracle.get_secret_key(self.owner);
    pedersen_hash([
    self.value,
    secret.low,
    secret.high,
    ])
    }

    Now:

     pub fn compute_nullifier(self, context: &mut PrivateContext) -> Field {
    let secret = context.request_nullifier_secret_key(self.owner);
    pedersen_hash([
    self.value,
    secret.low,
    secret.high,
    ])
    }
  2. New API compute_nullifier_without_context().

    This API is used within unconstrained functions where the private context is not available, and using an unverified nullifier key won't affect the network or other users. For example, it's used in compute_note_hash_and_nullifier() to compute values for the user's own notes.

    pub fn compute_nullifier_without_context(self) -> Field {
    let secret = oracle.get_nullifier_secret_key(self.owner);
    pedersen_hash([
    self.value,
    secret.low,
    secret.high,
    ])
    }

    Note that the get_secret_key oracle API has been renamed to get_nullifier_secret_key.

0.18.0

[Aztec.nr] Remove protocol_types from Nargo.toml

The protocol_types package is now being reexported from aztec. It can be accessed through dep::aztec::protocol_types.

aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.62.0", directory="yarn-project/aztec-nr/aztec" }

[Aztec.nr] key type definition in Map

The Map class now requires defining the key type in its declaration which must implement the ToField trait.

Before:

struct Storage {
balances: Map<PublicMutable<Field, FIELD_SERIALIZED_LEN>>
}

let user_balance = balances.at(owner.to_field())

Now:

struct Storage {
balances: Map<AztecAddress, PublicState<Field, FIELD_SERIALIZED_LEN>>
}

let user_balance = balances.at(owner)

[js] Updated function names

  • waitForSandbox renamed to waitForPXE in @aztec/aztec.js
  • getSandboxAccountsWallets renamed to getInitialTestAccountsWallets in @aztec/accounts/testing

0.17.0

[js] New @aztec/accounts package

Before:

import { getSchnorrAccount } from "@aztec/aztec.js"; // previously you would get the default accounts from the `aztec.js` package:

Now, import them from the new package @aztec/accounts

import { getSchnorrAccount } from "@aztec/accounts";

Typed Addresses

Address fields in Aztec.nr now is of type AztecAddress as opposed to Field

Before:

unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; VALUE_NOTE_LEN]) -> [Field; 4] {
let note_header = NoteHeader::new(_address, nonce, storage_slot);
...

Now:

unconstrained fn compute_note_hash_and_nullifier(
contract_address: AztecAddress,
nonce: Field,
storage_slot: Field,
serialized_note: [Field; VALUE_NOTE_LEN]
) -> pub [Field; 4] {
let note_header = NoteHeader::new(contract_address, nonce, storage_slot);

Similarly, there are changes when using aztec.js to call functions.

To parse a AztecAddress to BigInt, use .inner Before:

const tokenBigInt = await bridge.methods.token().simulate();

Now:

const tokenBigInt = (await bridge.methods.token().simulate()).inner;

[Aztec.nr] Add protocol_types to Nargo.toml

aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.62.0", directory="yarn-project/aztec-nr/aztec" }
protocol_types = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.62.0", directory="yarn-project/noir-protocol-circuits/crates/types"}

[Aztec.nr] moving compute_address func to AztecAddress

Before:

let calculated_address = compute_address(pub_key_x, pub_key_y, partial_address);

Now:

let calculated_address = AztecAddress::compute(pub_key_x, pub_key_y, partial_address);

[Aztec.nr] moving compute_selector to FunctionSelector

Before:

let selector = compute_selector("_initialize((Field))");

Now:

let selector = FunctionSelector::from_signature("_initialize((Field))");

[js] Importing contracts in JS

Contracts are now imported from a file with the type's name.

Before:

import { TokenContract } from "@aztec/noir-contracts/types";

Now:

import { TokenContract } from "@aztec/noir-contracts/Token";

[Aztec.nr] Aztec example contracts location change in Nargo.toml

Aztec contracts are now moved outside of the src folder, so you need to update your imports.

Before:

easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.16.9", directory = "noir-projects/noir-contracts/contracts/easy_private_token_contract"}

Now, just remove the src folder,:

easy_private_token_contract = {git = "https://github.com/AztecProtocol/aztec-packages/", tag ="v0.17.0", directory = "noir-projects/noir-contracts/contracts/easy_private_token_contract"}