Skip to main content

Using custom note types in Aztec.nr

It may be useful to write a custom note type if you want to use a specific type of private data or struct that does not have a default implementation in Aztec.nr. If you create a note that uses a custom note type, you are able to nullify that note with one nullifier. This is more secure and less expensive than using multiple separate notes.

As an example, if you are developing a card game, you may want to store multiple pieces of data in each card. Rather than storing each of these pieces of data in their own note, you can use a custom note to define the card, and then nullify (or exchange ownership of) the card when it has been used.

If you want to work with values or addresses, you can check out ValueNote or AddressNote.

Define a custom note type

You will likely want to define your note in a new file and import it into your contract.

A custom note type can be defined with the macro #[aztec(note)] used on a struct:

state_vars-CardNote
#[aztec(note)]
struct CardNote {
points: u8,
randomness: Field,
// The nullifying public key hash is used with the nsk_app to ensure that the note can be privately spent.
npk_m_hash: Field,
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L11-L19

In this example, we are implementing a card note that holds a number of points as u8.

Implement NoteInterface

You will need to implement a note interface for your note. Most of this is automatically generated by the #[aztec(note)] macro but can be overwritten. For your reference, this is what a note interface looks like:

note_interface
trait NoteInterface<let N: u32, let M: u32> {
// This function MUST be called with the correct note hash for consumption! It will otherwise silently fail and
// compute an incorrect value.
// The reason why we receive this as an argument instead of computing it ourselves directly is because the
// caller will typically already have computed this note hash, and we can reuse that value to reduce the total
// gate count of the circuit.
fn compute_nullifier(self, context: &mut PrivateContext, note_hash_for_nullify: Field) -> Field;

// Unlike compute_nullifier, this function does not take a note hash since it'll only be invoked in unconstrained
// contexts, where there is no gate count.
fn compute_nullifier_without_context(self) -> Field;

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn serialize_content(self) -> [Field; N];

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn deserialize_content(fields: [Field; N]) -> Self;

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn compute_note_hiding_point(self) -> Point;

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn get_header(self) -> NoteHeader;

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn set_header(&mut self, header: NoteHeader) -> ();

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn get_note_type_id() -> Field;

// Autogenerated by the #[aztec(note)] macro unless it is overridden by a custom implementation
fn to_be_bytes(self, storage_slot: Field) -> [u8; M];
}
Source code: noir-projects/aztec-nr/aztec/src/note/note_interface.nr#L5-L39

You will need to implement the functions compute_nullifier(...) and compute_nullifier_without_context() which tells Aztec how to compute nullifiers for your note.

note_interface
impl NoteInterface<CARD_NOTE_LEN, CARD_NOTE_BYTES_LEN> for CardNote {
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
)
}
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L29-L54

In this example, these functions compute the note hash by using compute_note_hash_for_nullify and then generate a nullifier by hashing this note hash together with a secret.

Methods

You will likely want to implement methods in order to use your note easily within contracts. For example, this may be what a new method can look like, for creating a new note:

cardnote_impl
impl CardNote {
pub fn new(points: u8, randomness: Field, npk_m_hash: Field) -> Self {
CardNote { points, randomness, npk_m_hash, header: NoteHeader::empty() }
}
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L21-L27

If you are also planning to be able to access the data with a note in public state, you will need to implement a method for serializing the note. This might look something like this:

serialize
impl Serialize<3> for CardNote {
fn serialize(self) -> [Field; 3] {
[self.points.to_field(), self.randomness, self.npk_m_hash.to_field()]
}
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L56-L62

Further reading