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 #[note]
used on a struct:
global CARD_NOTE_LEN: u32 = 3; // 3 plus a header.
#[note]
pub struct CardNote {
points: u8,
randomness: Field,
owner: AztecAddress,
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L12-L21
In this example, we are implementing a card note that holds a number of points
as u8
.
randomness
is not enforced by the protocol and should be implemented by the application developer. If you do not include randomness
, and the note preimage can be guessed by an attacker, it makes the note vulnerable to preimage attacks.
npk_m_hash
is nullifier public key, master, hash
which is the hash of the nullifier key owned by the user. It ensures that when the note is spent, only the owner of the npk_m
can spend it.
Implement NoteInterface
You will need to implement a note interface for your note. Most of this is automatically generated by the #[note]
macro but can be overwritten. For your reference, this is what a note interface looks like:
// Autogenerated by the #[note] macro
pub trait NoteInterface<let N: u32> {
// Autogenerated by the #[note] macro
fn serialize_content(self) -> [Field; N];
// Autogenerated by the #[note] macro
fn deserialize_content(fields: [Field; N]) -> Self;
// Autogenerated by the #[note] macro
fn get_header(self) -> NoteHeader;
// Autogenerated by the #[note] macro
fn set_header(&mut self, header: NoteHeader) -> ();
// Autogenerated by the #[note] macro
fn get_note_type_id() -> Field;
// Autogenerated by the #[note] macro
fn to_be_bytes(self, storage_slot: Field) -> [u8; N * 32 + 64];
// Autogenerated by the #[note] macro
fn compute_note_hash(self) -> Field;
}
Source code: noir-projects/aztec-nr/aztec/src/note/note_interface.nr#L32-L57
You will need to implement the functions compute_nullifier(...)
and compute_nullifier_without_context()
which tells Aztec how to compute nullifiers for your note.
impl NullifiableNote for CardNote {
fn compute_nullifier(
self,
context: &mut PrivateContext,
note_hash_for_nullify: Field,
) -> Field {
let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash();
let secret = context.request_nsk_app(owner_npk_m_hash);
poseidon2_hash_with_separator(
[note_hash_for_nullify, secret],
GENERATOR_INDEX__NOTE_NULLIFIER as Field,
)
}
unconstrained fn compute_nullifier_without_context(self) -> Field {
let note_hash_for_nullify = compute_note_hash_for_nullify(self);
let owner_npk_m_hash: Field = get_public_keys(self.owner).npk_m.hash();
let secret = get_nsk_app(owner_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#L31-L56
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:
impl CardNote {
pub fn new(points: u8, randomness: Field, owner: AztecAddress) -> Self {
CardNote { points, randomness, owner, header: NoteHeader::empty() }
}
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L23-L29
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:
impl Serialize<3> for CardNote {
fn serialize(self) -> [Field; 3] {
[self.points.to_field(), self.randomness, self.owner.to_field()]
}
}
Source code: noir-projects/noir-contracts/contracts/docs_example_contract/src/types/card_note.nr#L58-L64