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.

TBD

Autogenerate compute_note_hash_and_nullifier

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).

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/src/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: ImmutableSingleton<CardNote, CARD_NOTE_LEN>,
}

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

Now:

struct Storage {
leader: PublicState<Leader>,
legendary_card: Singleton<CardNote>,
profiles: Map<AztecAddress, Singleton<CardNote>>,
test: Set<CardNote>,
imm_singleton: ImmutableSingleton<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: PublicState::new(
context,
1
),
legendary_card: Singleton::new(context, 2),
profiles: Map::new(
context,
3,
|context, slot| {
Singleton::new(context, slot)
},
),
test: Set::new(context, 4),
imm_singleton: ImmutableSingleton::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.24.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<PublicState<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().view();

Now:

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

[Aztec.nr] Add protocol_types to Nargo.toml

aztec = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.24.0", directory="yarn-project/aztec-nr/aztec" }
protocol_types = { git="https://github.com/AztecProtocol/aztec-packages/", tag="aztec-packages-v0.24.0", directory="yarn-project/noir-protocol-circuits/src/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 = "yarn-project/noir-contracts/src/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 = "yarn-project/noir-contracts/contracts/easy_private_token_contract"}