Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@
"noirup",
"allow_phase_change",
"nophasecheck",
"nullifer",
"Nullifiable",
"offchain",
"onchain",
Expand Down Expand Up @@ -367,6 +366,7 @@
"unfinalized",
"uniquified",
"uniquify",
"unlinkability",
"unkonstrained",
"unnullify",
"unpadded",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2689,7 +2689,7 @@ Doing the changes is as straightforward as:

`UintNote` has also been updated to use the native `u128` type.

### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer`
### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier`

This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2948,7 +2948,7 @@ Doing the changes is as straightforward as:

`UintNote` has also been updated to use the native `u128` type.

### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer`
### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier`

This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2948,7 +2948,7 @@ Doing the changes is as straightforward as:

`UintNote` has also been updated to use the native `u128` type.

### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifer`
### [aztec-nr] Removed `compute_note_hash_and_optionally_a_nullifier`

This function is no longer mandatory for contracts, and the `#[aztec]` macro no longer injects it.

Expand Down
85 changes: 35 additions & 50 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -349,71 +349,56 @@ impl PrivateContext {
self.note_hashes.push(Counted::new(note_hash, self.next_counter()));
}

/// Pushes a new nullifier to the Aztec blockchain's global Nullifier Tree (a state tree).
/// Creates a new [nullifier](crate::nullifier).
///
/// See also: `push_nullifier_for_note_hash`.
/// ## Safety
///
/// Low-level function: Ordinarily, smart contract developers will not need to manually call this. Aztec-nr's state
/// variables (see `../state_vars/`) are designed to understand when to create and push new nullifiers.
///
/// A nullifier can only be emitted once. Duplicate nullifier insertions are rejected by the protocol.
///
/// Generally, a nullifier is emitted to prevent an action from happening more than once, in such a way that the
/// action cannot be linked (by an observer of the blockchain) to any earlier transactions.
///
/// I.e. a nullifier is a random-looking, but deterministic record of a private, one-time action, which does not
/// leak what action has been taken, and which preserves the property of "tx unlinkability".
/// This is a low-level function that must be used with great care to avoid subtle corruption of contract state.
/// Instead of calling this function, consider using the higher-level [`crate::state_vars::SingleUseClaim`].
///
/// Usually, a nullifier will be emitted to "spend" a note (a piece of private state), without revealing which
/// specific note is being spent.
/// In particular, callers must ensure all nullifiers created by a contract are properly domain-separated, so that
/// unrelated components don't interfere with one another (e.g. a transaction nullifier accidentally marking a
/// variable as initialized). Only [`PrivateContext::push_nullifier_for_note_hash`] should be used for note
/// nullifiers, never this one.
///
/// (Important: in such cases, use the below `push_nullifier_for_note_hash`).
///
/// Sometimes, a nullifier might be emitted completely unrelated to any notes. Examples include initialization of a
/// new contract; initialization of a PrivateMutable, or signalling in Semaphore-like applications. This
/// `push_nullifier` function serves such use cases.
///
/// # Arguments
/// * `nullifier`
///
/// # Advanced
/// From here, the protocol's kernel circuits will take over and insert the nullifier into the protocol's
/// "nullifier tree" (in the Base Rollup circuit). Before insertion, the protocol will:
/// - "Silo" the `nullifier` with the contract address of this function, to yield a `siloed_nullifier`. This
/// prevents state collisions between different smart contracts.
/// - Ensure the `siloed_nullifier` is unique (the nullifier tree is an indexed merkle tree which supports
/// efficient non-membership proofs).
/// ## Advanced
///
/// The raw `nullifier` is not what is inserted into the Aztec state tree: it will be first siloed by contract
/// address via [`crate::protocol::hash::compute_siloed_nullifier`] in order to prevent accidental or malicious
/// interference of nullifiers from different contracts.
pub fn push_nullifier(&mut self, nullifier: Field) {
notify_created_nullifier(nullifier);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: 0 }.count(self.next_counter()));
}

/// Pushes a nullifier that corresponds to a specific note hash.
/// Creates a new [nullifier](crate::nullifier) associated with a note.
///
/// Low-level function: Ordinarily, smart contract developers will not need to manually call this. Aztec-nr's state
/// variables (see `../state_vars/`) are designed to understand when to create and push new nullifiers.
/// This is a variant of [`PrivateContext::push_nullifier`] that is used for note nullifiers, i.e. nullifiers that
/// correspond to a note. If a note and its nullifier are created in the same transaction, then the private kernels
/// will 'squash' these values, deleting them both as if they never existed and reducing transaction fees.
///
/// This is a specialized version of `push_nullifier` that links a nullifier to the specific note hash it's
/// nullifying. This is the most common usage pattern for nullifiers. See `push_nullifier` for more explanation on
/// nullifiers.
/// The `nullification_note_hash` must be the result of calling
/// [`crate::note::utils::compute_confirmed_note_hash_for_nullification`] for pending notes, and `0` for settled
/// notes (which cannot be squashed).
///
/// # Arguments
/// * `nullifier`
/// * `nullified_note_hash` - The note hash of the note being nullified
/// ## Safety
///
/// # Advanced
///Important: usage of this function doesn't mean that the world will _see_ that this nullifier relates to the
/// given nullified_note_hash (as that would violate "tx unlinkability"); it simply informs the user's PXE about
/// the relationship (via `notify_nullified_note`). The PXE can then use this information to feed hints to the
/// kernel circuits for "squashing" purposes: If a note is nullified during the same tx which created it, we can
/// "squash" (delete) the note and nullifier (and any private logs associated with the note), to save on data
/// emission costs.
///
pub fn push_nullifier_for_note_hash(&mut self, nullifier: Field, nullified_note_hash: Field) {
/// This is a low-level function that must be used with great care to avoid subtle corruption of contract state.
/// Instead of calling this function, consider using the higher-level [`crate::note::lifecycle::destroy_note`].
///
/// The precautions listed for [`PrivateContext::push_nullifier`] apply here as well, and callers should
/// additionally ensure `nullification_note_hash` corresponds to a note emitted by this contract, with its hash
/// computed in the same transaction execution phase as the call to this function. Finally, only this function
/// should be used for note nullifiers, never [`PrivateContext::push_nullifier`].
///
/// Failure to do these things can result in unprovable contexts, accidental deletion of notes, or double-spend
/// attacks.
pub fn push_nullifier_for_note_hash(&mut self, nullifier: Field, nullification_note_hash: Field) {
let nullifier_counter = self.next_counter();
notify_nullified_note(nullifier, nullified_note_hash, nullifier_counter);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: nullified_note_hash }.count(nullifier_counter));
notify_nullified_note(nullifier, nullification_note_hash, nullifier_counter);
self.nullifiers.push(Nullifier { value: nullifier, note_hash: nullification_note_hash }.count(
nullifier_counter,
));
}

/// Returns the anchor block header - the historical block header that this private function is reading from.
Expand Down
28 changes: 16 additions & 12 deletions noir-projects/aztec-nr/aztec/src/context/public_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -366,22 +366,26 @@ impl PublicContext {
unsafe { avm::emit_note_hash(note_hash) };
}

/// Adds a new nullifier to the Aztec blockchain's global Nullifier Tree.
/// Creates a new [nullifier](crate::nullifier).
///
/// Whilst nullifiers are primarily intended as a _privacy-preserving_ record of a one-time action, they can also
/// be used to efficiently record _public_ one-time actions too. Hence why you're seeing this function within the
/// PublicContext. An example is to check whether a contract has been published: we emit a nullifier that is
/// deterministic, but whose preimage is _not_ private.
/// While nullifiers are primarily intended as a _privacy-preserving_ record of a one-time action, they can also
/// be used to efficiently record _public_ one-time actions. This function allows creating nullifiers from public
/// contract functions, which behave just like those created from private functions.
///
/// # Arguments
/// * `nullifier` - A unique field element that represents the consumed state
/// ## Safety
///
/// # Advanced
/// * Nullifier is immediately added to the global nullifier tree
/// * Emitted nullifiers are immediately visible to all subsequent transactions in the same block
/// * Automatically siloed with the contract address by the protocol
/// * Used for preventing double-spending and ensuring one-time actions
/// This is a low-level function that must be used with great care to avoid subtle corruption of contract state.
///
/// In particular, callers must ensure all nullifiers created by a contract are properly domain-separated, so that
/// unrelated components don't interfere with one another (e.g. a transaction nullifier accidentally marking a
/// variable as initialized). Note nullifiers should only be created via
/// [`crate::context::PrivateContext::push_nullifier_for_note_hash`].
///
/// ## Advanced
///
/// The raw `nullifier` is not what is inserted into the Aztec state tree: it will be first siloed by contract
/// address via [`crate::protocol::hash::compute_siloed_nullifier`] in order to prevent accidental or malicious
/// interference of nullifiers from different contracts.
pub fn push_nullifier(_self: Self, nullifier: Field) {
// Safety: AVM opcodes are constrained by the AVM itself
unsafe { avm::emit_nullifier(nullifier) };
Expand Down
49 changes: 48 additions & 1 deletion noir-projects/aztec-nr/aztec/src/nullifier/mod.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
//! Nullifier-related utilities.

//!
//! Nullifiers are one of the key primitives of private state. A nullifier is a `Field` value that is stored in one of
//! the Aztec state trees: the nullifier tree. Only unique values can be inserted into this tree: attempting to create
//! an
//! already existing nullifier (a duplicate nullifier) will result in either the transaction being unprovable, invalid,
//! or reverting, depending on exactly when the duplicate is created.
//!
//! Generally, nullifiers are used to prevent an action from happening more than once, or to more generally 'consume' a
//! resource. This can include preventing re-initialization of contracts, replay attacks of signatures, repeated claims
//! of a deposit, double-spends of received funds, etc. To achieve this, nullifiers must be computed
//! **deterministically** from the resource they're consuming. For example a contract initialization nullifier might
//! use
//! its address, or a signature replay protection could use the signature hash.
//!
//! One of the key properties of nullifiers is that they can be created by private functions, resulting in transactions
//! that do not reveal which actions they've performed. Their computation often involves a **secret parameter**, often
//! derived from a nullifier hiding key (`nhk`) which prevents linking of the resource that was consumed from the
//! nullifier. For example, it is not possible to determine which nullifier corresponds to a given note hash without
//! knowledge of the `nhk`, and so the transactions that created the note and nullifier remain unlinked.
Comment on lines +16 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

//!
//! In other words, a nullifier is (in most cases) a random-looking but deterministic record of a private, one-time
//! action, which does not leak what action has been taken, and which preserves the property of transaction
//! unlinkability.
//!
//! In some cases, nullifiers cannot be secret as knowledge of them **must** be public information. For example,
//! contracts used by multiple people (like tokens) cannot have secrets in their initialization nullifiers: for users
//! to
//! use the contract they must prove that it has been initialized, and this requires them being able to compute the
//! initialization nullifier.
//!
//! ## Nullifier Creation
//!
//! The low-level mechanisms to create new nullifiers are [`crate::context::PrivateContext::push_nullifier`] and
//! [`crate::context::PublicContext::push_nullifier`], but these require care and can be hard to use correctly.
//! Higher-level abstractions exist which safely create nullifiers, such as [`crate::note::lifecycle::destroy_note`]
//! and
//! [`crate::state_vars::SingleUseClaim`].
//!
//! ## Reading Nullifiers
//!
//! Private functions can prove that nullifiers have been created via
//! [`crate::context::PrivateContext::assert_nullifier_exists`] and
//! [`crate::history::nullifier::assert_nullifier_existed_by`], but the only general mechanism to privately prove that
//! a
//! nullifier _does not_ exist is to create it - which can only be done once.
//!
//! Public functions on the other hand can prove both nullifier existence and non-existence via
//! [`crate::context::PublicContext::nullifier_exists_unsafe`].
pub mod utils;
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,77 @@ pub contract Claim {
// docs:end:history_import
use aztec::{
macros::{functions::{external, initializer}, storage::storage},
note::{
HintedNote, note_interface::NoteHash,
utils::compute_confirmed_note_hash_for_nullification,
},
protocol::address::AztecAddress,
state_vars::PublicImmutable,
note::{HintedNote, utils::compute_confirmed_note_hash_for_nullification},
protocol::{address::AztecAddress, traits::Packable},
state_vars::{Map, Owned, PublicImmutable, SingleUseClaim},
};
use token::Token;
use uint_note::UintNote;

// TODO: This can be optimized by storing the addresses in Config struct in 1 PublicImmutable (less merkle proofs).
#[storage]
struct Storage<Context> {
#[derive(Eq, Packable)]
struct ClaimConfig {
// Address of a contract based on whose notes we distribute the rewards
target_contract: PublicImmutable<AztecAddress, Context>,
target_contract: AztecAddress,
// Token to be distributed as a reward when claiming
reward_token: PublicImmutable<AztecAddress, Context>,
reward_token: AztecAddress,
}

#[storage]
struct Storage<Context> {
config: PublicImmutable<ClaimConfig, Context>,
// Maps a note hash to owner-specific single-use claims, preventing double-claiming of rewards.
note_hash_claims: Map<Field, Owned<SingleUseClaim<Context>, Context>, Context>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment describing what the Map's key Field represents would be nice here

}

#[external("public")]
#[initializer]
fn constructor(target_contract: AztecAddress, reward_token: AztecAddress) {
self.storage.target_contract.initialize(target_contract);
self.storage.reward_token.initialize(reward_token);
self.storage.config.initialize(ClaimConfig { target_contract, reward_token });
}

#[external("private")]
fn claim(hinted_note: HintedNote<UintNote>, recipient: AztecAddress) {
// 1) Check that the note corresponds to the target contract
let target_address = self.storage.target_contract.read();
// 1) Prove that the note exists
// docs:start:prove_note_inclusion
let header = self.context.get_anchor_block_header();
let confirmed_note = assert_note_existed_by(header, hinted_note);
// docs:end:prove_note_inclusion

let config = self.storage.config.read();

// 2) Check that the note corresponds to the target contract
assert(
target_address == hinted_note.contract_address,
config.target_contract == confirmed_note.contract_address,
"Note does not correspond to the target contract",
);

// 2) Check that the note is owned by the msg_sender
assert(hinted_note.owner == self.msg_sender(), "Note is not owned by the msg_sender");
// 3) Check that the note is owned by the msg_sender
assert(confirmed_note.owner == self.msg_sender(), "Note is not owned by the msg_sender");

// Given that there is only 1 state variable in the Crowdfunding contract we don't need to constrain the storage
// slot of the note as there is no risk of claiming with a note that is not a donation note.

// 3) Prove that the note hash exists in the note hash tree
// docs:start:prove_note_inclusion
let header = self.context.get_anchor_block_header();
let confirmed_note = assert_note_existed_by(header, hinted_note);
// docs:end:prove_note_inclusion

// 4) Compute and emit a nullifier which is unique to the note and this contract to ensure the reward can be
// claimed only once with the given note.
// Note: Only the owner of the npk_m will be able to produce the nhk_app and compute this nullifier.
// The nullifier is unique to the note and THIS contract because the protocol siloes all nullifiers with
// the address of a contract it was emitted from.
// TODO(#7775): manually computing the hash and passing it to compute_nullifier func is not great as note could
// handle it on its own or we could make assert_note_existed_by return note_hash_for_nullification.
// 4) Consume the claim of this note, ensuring the reward can be claimed only once with the given note. Each
// claim being tied to its owner results in unlinkability of the claim's nullifier and the underlying note
// hash via the owner's nullifier hiding key (`nhk`).
//
// Note: we're using the note's hash as the claim identifier, as it serves as a unique identifier (because we
// know the note is settled, and hence its note hash unique). This is not the same thing as nullifying the note
// however, which can only be done from the contract that emitted the note. From the point of view of the
// Crowdfunding contract, the note is still active.
let note_hash_for_nullification =
compute_confirmed_note_hash_for_nullification(confirmed_note);
let nullifier = hinted_note.note.compute_nullifier(
self.context,
hinted_note.owner,
note_hash_for_nullification,
);
self.context.push_nullifier(nullifier);
self
.storage
.note_hash_claims
.at(note_hash_for_nullification)
.at(confirmed_note.owner)
.claim();

// 5) Finally we mint the reward token to the sender of the transaction
self.enqueue(Token::at(self.storage.reward_token.read()).mint_to_public(
// 5) Finally we mint the reward token to the requested recipient
self.enqueue(Token::at(config.reward_token).mint_to_public(
recipient,
hinted_note.note.value,
confirmed_note.note.value,
));
}
}
Loading
Loading