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 aTxSimulationResult
, 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 aContext
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
andaztec sandbox
: nowaztec start --sandbox
aztec-builder
: nowaztec codegen
andaztec update
ADDED:
aztec test [options]
: runsaztec 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
, andToRadix
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 towallet.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!
- Only fixed length types are supported
- All numeric types become Fields
- Strings become arrays of Fields, one per char
- Arrays become arrays of Fields following rules 2 and 3
- 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
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:
Singleton
->PrivateMutable
ImmutableSingleton
->PrivateImmutable
StablePublicState
->SharedImmutable
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 Map
s 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
-
Changing
compute_nullifier()
tocompute_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,
])
} -
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 toget_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 towaitForPXE
in@aztec/aztec.js
getSandboxAccountsWallets
renamed togetInitialTestAccountsWallets
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"}