Skip to main content
Version: v3.0.0-nightly.20251206

Partial Notes

What are Partial Notes?

Partial notes are notes created with incomplete data, usually during private execution, which can be completed with additional information that becomes available later, usually during public execution.

Let's say, for example, we have a UintNote:

uint_note_def
#[derive(Deserialize, Eq, Serialize, Packable)]
#[custom_note]
pub struct UintNote {
/// The number stored in the note.
value: u128,
}
Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L27-L34

The UintNote struct itself only contains the value field. Additional fields including owner, randomness, and storage_slot are passed as parameters during note hash computation.

When creating the note locally during private execution, the owner and storage_slot are known, but the value potentially is not (e.g., it depends on some onchain dynamic variable). First, a partial note can be created during private execution that commits to the owner, randomness, and storage_slot, and then the note is "completed" to create a full note by later adding the value field, usually during public execution.

Use Cases

Partial notes are useful when a e.g., part of the note struct is a value that depends on dynamic, public onchain data that isn't available during private execution, such as:

  • AMM swap prices
  • Current gas prices
  • Time-dependent interest accrual

Implementation

All notes in Aztec use the partial note format internally. This ensures that notes produce identical note hashes regardless of whether they were created as complete notes (with all fields known in private) or as partial notes (completed later in public). By having all notes follow the same two-phase hash commitment process, the protocol maintains consistency and allows notes created through different flows to behave identically.

Note Structure Example

The UintNote struct contains only the value field:

uint_note_def
#[derive(Deserialize, Eq, Serialize, Packable)]
#[custom_note]
pub struct UintNote {
/// The number stored in the note.
value: u128,
}
Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L27-L34

Two-Phase Commitment Process

Phase 1: Partial Commitment (Private Execution)

The private fields (owner, randomness, and storage_slot) are committed during local, private execution:

compute_partial_commitment
fn compute_partial_commitment(
owner: AztecAddress,
storage_slot: Field,
randomness: Field,
) -> Field {
poseidon2_hash_with_separator(
[owner.to_field(), storage_slot, randomness],
GENERATOR_INDEX__NOTE_HASH,
)
}
Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L168-L179

This creates a partial note commitment:

partial_commitment = H(owner, storage_slot, randomness)

Phase 2: Note Completion (Public Execution)

The note is completed by hashing the partial commitment with the public value:

compute_complete_note_hash
fn compute_complete_note_hash(self, value: u128) -> Field {
// Here we finalize the note hash by including the (public) value into the partial note commitment. Note that we
// use the same generator index as we used for the first round of poseidon - this is not an issue.
poseidon2_hash_with_separator(
[self.commitment, value.to_field()],
GENERATOR_INDEX__NOTE_HASH,
)
}
Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L292-L301

The resulting structure is a nested commitment:

note_hash = H(H(owner, storage_slot, randomness), value)
= H(partial_commitment, value)

Universal Note Format

All notes in Aztec use the partial note format internally, even when all data is known during private execution. This ensures consistent note hash computation regardless of how the note was created.

When a note is created with all fields known (including owner, storage_slot, randomness, and value):

  1. A partial commitment is computed from the private fields (owner, storage_slot, randomness)
  2. The partial commitment is immediately completed with the value field
compute_note_hash
fn compute_note_hash(
self,
owner: AztecAddress,
storage_slot: Field,
randomness: Field,
) -> Field {
// Partial notes can be implemented by having the note hash be either the result of multiscalar multiplication
// (MSM), or two rounds of poseidon. MSM results in more constraints and is only required when multiple variants
// of partial notes are supported. Because UintNote has just one variant (where the value is public), we use
// poseidon instead.

// We must compute the same note hash as would be produced by a partial note created and completed with the same
// values, so that notes all behave the same way regardless of how they were created. To achieve this, we
// perform both steps of the partial note computation.

// First we create the partial note from a commitment to the private content (including storage slot).
let partial_note = PartialUintNote {
commitment: compute_partial_commitment(owner, storage_slot, randomness),
};

// Then compute the completion note hash. In a real partial note this step would be performed in public.
partial_note.compute_complete_note_hash(self.value)
}
Source code: noir-projects/aztec-nr/uint-note/src/uint_note.nr#L37-L61

This two-step process ensures that notes with identical field values produce identical note hashes, regardless of whether they were created as partial notes or complete notes.

Partial Notes in Practice

To understand how to use partial notes in practice, this AMM contract uses partial notes to initiate and complete the swap of token1 to token2. Since the exchange rate is onchain, it cannot be known ahead of time while executing in private so a full note cannot be created. Instead, a partial note is created for the owner swapping the tokens. This partial note is then completed during public execution once the exchange rate can be read.