From c2ef6e821547cec0acfd88e348fabdf1e65d5791 Mon Sep 17 00:00:00 2001 From: nventuro <2530770+nventuro@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:47:40 +0000 Subject: [PATCH] refactor!: improve the `aztec::history` module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long overdue. This clears up the `history` mod, removing the unnecessary traits and exposing functions instead, fixing lots of bad naming that confused people, addressing some clarifications re. siloed vs unsiloed params, and adding some docs where it was most needed. Fixes F-240. Built with Claude :robot: (the docs are mine) Co-authored-by: Jan Beneš Co-authored-by: Nicolás Venturo --- .../advanced/how_to_prove_history.md | 60 ++++---- .../docs/resources/migration_notes.md | 31 +++- .../aztec/src/context/private_context.nr | 12 +- .../aztec/src/history/contract_inclusion.nr | 71 --------- .../aztec-nr/aztec/src/history/deployment.nr | 56 +++++++ .../aztec-nr/aztec/src/history/mod.nr | 21 ++- .../aztec-nr/aztec/src/history/note.nr | 109 ++++++++++++++ .../history/{note_validity => note}/test.nr | 35 ++++- .../aztec/src/history/note_inclusion.nr | 55 ------- .../aztec/src/history/note_inclusion/test.nr | 29 ---- .../aztec/src/history/note_validity.nr | 29 ---- .../aztec-nr/aztec/src/history/nullifier.nr | 127 ++++++++++++++++ .../aztec/src/history/nullifier/test.nr | 138 ++++++++++++++++++ .../aztec/src/history/nullifier_inclusion.nr | 78 ---------- .../src/history/nullifier_inclusion/test.nr | 74 ---------- .../src/history/nullifier_non_inclusion.nr | 97 ------------ .../history/nullifier_non_inclusion/test.nr | 74 ---------- .../aztec/src/history/public_storage.nr | 42 ------ .../aztec-nr/aztec/src/history/storage.nr | 34 +++++ .../{public_storage => storage}/test.nr | 11 +- .../aztec-nr/aztec/src/note/confirmed_note.nr | 2 +- .../aztec-nr/aztec/src/note/hinted_note.nr | 2 +- noir-projects/aztec-nr/aztec/src/note/mod.nr | 3 + .../aztec-nr/aztec/src/utils/with_hash.nr | 4 +- .../aztec-nr/uint-note/src/uint_note.nr | 9 +- .../contracts/app/claim_contract/src/main.nr | 6 +- .../contracts/test/test_contract/src/main.nr | 4 +- .../src/test/deployment_proofs.nr | 56 +++++-- 28 files changed, 641 insertions(+), 628 deletions(-) delete mode 100644 noir-projects/aztec-nr/aztec/src/history/contract_inclusion.nr create mode 100644 noir-projects/aztec-nr/aztec/src/history/deployment.nr create mode 100644 noir-projects/aztec-nr/aztec/src/history/note.nr rename noir-projects/aztec-nr/aztec/src/history/{note_validity => note}/test.nr (52%) delete mode 100644 noir-projects/aztec-nr/aztec/src/history/note_inclusion.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/note_inclusion/test.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/note_validity.nr create mode 100644 noir-projects/aztec-nr/aztec/src/history/nullifier.nr create mode 100644 noir-projects/aztec-nr/aztec/src/history/nullifier/test.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion/test.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion/test.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/history/public_storage.nr create mode 100644 noir-projects/aztec-nr/aztec/src/history/storage.nr rename noir-projects/aztec-nr/aztec/src/history/{public_storage => storage}/test.nr (81%) diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md index 60ca53ff58b2..2e54b20ab5f4 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md @@ -20,7 +20,7 @@ You can create proofs for these elements at any past block height: - **Note inclusion** - prove a note existed in the note hash tree - **Note validity** - prove a note existed and wasn't nullified at a specific block - **Nullifier inclusion/non-inclusion** - prove a nullifier was or wasn't in the nullifier tree -- **Contract deployment** - prove a contract was deployed or initialized +- **Contract deployment** - prove a contract's bytecode was published or initialized Common use cases: - Verify ownership of an asset from another contract without revealing which specific note @@ -29,7 +29,7 @@ Common use cases: ## Prove note inclusion -Import the trait: +Import the function: #include_code history_import noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr rust @@ -42,10 +42,10 @@ Prove a note exists in the note hash tree: To prove a note was valid (existed AND wasn't nullified) at a historical block: ```rust -use dep::aztec::history::note_validity::ProveNoteValidity; +use dep::aztec::history::note::assert_note_was_valid_by; let header = self.context.get_anchor_block_header(); -header.prove_note_validity(hinted_note, &mut self.context); +assert_note_was_valid_by(header, hinted_note, &mut self.context); ``` This verifies both: @@ -57,8 +57,10 @@ This verifies both: To prove against state at a specific past block (not just the anchor block): ```rust +use dep::aztec::history::note::assert_note_existed_by; + let historical_header = self.context.get_block_header_at(block_number); -historical_header.prove_note_inclusion(hinted_note); +assert_note_existed_by(historical_header, hinted_note); ``` :::warning @@ -70,45 +72,45 @@ Using `get_block_header_at` adds ~3k constraints to prove Archive tree membershi To prove a note has been spent/nullified: ```rust -use dep::aztec::history::nullifier_inclusion::ProveNoteIsNullified; +use dep::aztec::history::note::assert_note_was_nullified_by; let header = self.context.get_anchor_block_header(); -header.prove_note_is_nullified(hinted_note, &mut self.context); +assert_note_was_nullified_by(header, confirmed_note, &mut self.context); ``` -## Prove contract deployment +## Prove contract bytecode was published -To prove a contract was deployed at a historical block: +To prove a contract's bytecode was published at a historical block: ```rust -use dep::aztec::history::contract_inclusion::ProveContractDeployment; +use dep::aztec::history::deployment::assert_contract_bytecode_was_published_by; let header = self.context.get_anchor_block_header(); -header.prove_contract_deployment(contract_address); +assert_contract_bytecode_was_published_by(header, contract_address); ``` You can also prove a contract was initialized (constructor was called): ```rust -use dep::aztec::history::contract_inclusion::ProveContractInitialization; +use dep::aztec::history::deployment::assert_contract_was_initialized_by; let header = self.context.get_anchor_block_header(); -header.prove_contract_initialization(contract_address); +assert_contract_was_initialized_by(header, contract_address); ``` -## Available proof traits - -The `aztec::history` module provides these traits: - -| Trait | Purpose | -|-------|---------| -| `ProveNoteInclusion` | Prove note exists in note hash tree | -| `ProveNoteValidity` | Prove note exists and is not nullified | -| `ProveNoteIsNullified` | Prove note's nullifier is in nullifier tree | -| `ProveNoteNotNullified` | Prove note's nullifier is not in nullifier tree | -| `ProveNullifierInclusion` | Prove a raw nullifier exists | -| `ProveNullifierNonInclusion` | Prove a raw nullifier does not exist | -| `ProveContractDeployment` | Prove a contract was deployed | -| `ProveContractNonDeployment` | Prove a contract was not deployed | -| `ProveContractInitialization` | Prove a contract was initialized | -| `ProveContractNonInitialization` | Prove a contract was not initialized | +## Available proof functions + +The `aztec::history` module provides these functions: + +| Function | Module | Purpose | +|----------|--------|---------| +| `assert_note_existed_by` | `history::note` | Prove note exists in note hash tree | +| `assert_note_was_valid_by` | `history::note` | Prove note exists and is not nullified | +| `assert_note_was_nullified_by` | `history::note` | Prove note's nullifier is in nullifier tree | +| `assert_note_was_not_nullified_by` | `history::note` | Prove note's nullifier is not in nullifier tree | +| `assert_nullifier_existed_by` | `history::nullifier` | Prove a raw nullifier exists | +| `assert_nullifier_did_not_exist_by` | `history::nullifier` | Prove a raw nullifier does not exist | +| `assert_contract_bytecode_was_published_by` | `history::deployment` | Prove a contract's bytecode was published | +| `assert_contract_bytecode_was_not_published_by` | `history::deployment` | Prove a contract's bytecode was not published | +| `assert_contract_was_initialized_by` | `history::deployment` | Prove a contract was initialized | +| `assert_contract_was_not_initialized_by` | `history::deployment` | Prove a contract was not initialized | diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index a89e869c1731..d2d11cce0588 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,35 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [aztec-nr] History module refactored to use standalone functions + +The `aztec::history` module has been refactored to use standalone functions instead of traits. This changes the calling convention from method syntax to function syntax. + +```diff +- use dep::aztec::history::note_inclusion::ProveNoteInclusion; ++ use dep::aztec::history::note::assert_note_existed_by; + +let block_header = context.get_anchor_block_header(); +- let confirmed_note = block_header.prove_note_inclusion(hinted_note); ++ let confirmed_note = assert_note_existed_by(block_header, hinted_note); +``` + +**Function name and module mapping:** + +| Old (trait method) | New (standalone function) | +|--------------------|---------------------------| +| `history::note_inclusion::prove_note_inclusion` | `history::note::assert_note_existed_by` | +| `history::note_validity::prove_note_validity` | `history::note::assert_note_was_valid_by` | +| `history::nullifier_inclusion::prove_nullifier_inclusion` | `history::nullifier::assert_nullifier_existed_by` | +| `history::nullifier_inclusion::prove_note_is_nullified` | `history::note::assert_note_was_nullified_by` | +| `history::nullifier_non_inclusion::prove_nullifier_non_inclusion` | `history::nullifier::assert_nullifier_did_not_exist_by` | +| `history::nullifier_non_inclusion::prove_note_not_nullified` | `history::note::assert_note_was_not_nullified_by` | +| `history::contract_inclusion::prove_contract_deployment` | `history::deployment::assert_contract_bytecode_was_published_by` | +| `history::contract_inclusion::prove_contract_non_deployment` | `history::deployment::assert_contract_bytecode_was_not_published_by` | +| `history::contract_inclusion::prove_contract_initialization` | `history::deployment::assert_contract_was_initialized_by` | +| `history::contract_inclusion::prove_contract_non_initialization` | `history::deployment::assert_contract_was_not_initialized_by` | +| `history::public_storage::public_storage_historical_read` | `history::storage::public_storage_historical_read` | + ### [Aztec.js] Transaction sending API redesign The old chained `.send().wait()` pattern has been replaced with a single `.send(options)` call that handles both sending and waiting. @@ -4308,7 +4337,7 @@ await expect( ### [Aztec.nr] Public storage historical read API improvement -`history::public_value_inclusion::prove_public_value_inclusion` has been renamed to `history::public_storage::public_storage_historical_read`, and its API changed slightly. Instead of receiving a `value` parameter it now returns the historical value stored at that slot. +`history::public_value_inclusion::prove_public_value_inclusion` has been renamed to `history::storage::public_storage_historical_read`, and its API changed slightly. Instead of receiving a `value` parameter it now returns the historical value stored at that slot. If you were using an oracle to get the value to pass to `prove_public_value_inclusion`, drop the oracle and use the return value from `public_storage_historical_read` instead: diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index e98aaadf5065..9b3effee810e 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -746,9 +746,9 @@ impl PrivateContext { /// /// ## Historical Notes /// - /// If you need to assert that a note existed _at some specific time in the past_, instead of simply proving that it - /// exists in the current anchor block, use - /// [crate::history::note_inclusion::ProveNoteInclusion::prove_note_inclusion] instead. + /// If you need to assert that a note existed _by some specific block in the past_, instead of simply proving that it + /// exists by the current anchor block, use + /// [crate::history::note::assert_note_existed_by] instead. /// /// ## Cost /// @@ -784,9 +784,9 @@ impl PrivateContext { /// /// ## Historical Nullifiers /// - /// If you need to assert that a nullifier existed _at some specific time in the past_, instead of simply proving - /// that it exists in the current anchor block, use - /// [crate::history::nullifier_inclusion::ProveNullifierInclusion::prove_nullifier_inclusion] instead. + /// If you need to assert that a nullifier existed _by some specific block in the past_, instead of simply proving + /// that it exists by the current anchor block, use + /// [crate::history::nullifier::assert_nullifier_existed_by] instead. /// /// ## Public vs Private /// diff --git a/noir-projects/aztec-nr/aztec/src/history/contract_inclusion.nr b/noir-projects/aztec-nr/aztec/src/history/contract_inclusion.nr deleted file mode 100644 index d8e4b0484853..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/contract_inclusion.nr +++ /dev/null @@ -1,71 +0,0 @@ -use dep::protocol_types::{ - abis::block_header::BlockHeader, address::AztecAddress, - constants::CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, hash::compute_siloed_nullifier, - traits::ToField, -}; - -use crate::history::{ - nullifier_inclusion::ProveNullifierInclusion, - nullifier_non_inclusion::ProveNullifierNonInclusion, -}; - -// This is tested in `noir-projects/noir-contracts/test_contract/src/test.nr because we cannot define a contract -// from within aztec.nr (due to the contract macro). - -pub trait ProveContractDeployment { - fn prove_contract_deployment(header: BlockHeader, contract_address: AztecAddress); -} - -impl ProveContractDeployment for BlockHeader { - fn prove_contract_deployment(self, contract_address: AztecAddress) { - // Compute deployment nullifier - let nullifier = compute_siloed_nullifier( - CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, - contract_address.to_field(), - ); - - self.prove_nullifier_inclusion(nullifier); - } -} - -pub trait ProveContractNonDeployment { - fn prove_contract_non_deployment(header: BlockHeader, contract_address: AztecAddress); -} - -impl ProveContractNonDeployment for BlockHeader { - fn prove_contract_non_deployment(self, contract_address: AztecAddress) { - // Compute deployment nullifier - let nullifier = compute_siloed_nullifier( - CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, - contract_address.to_field(), - ); - - self.prove_nullifier_non_inclusion(nullifier); - } -} - -pub trait ProveContractInitialization { - fn prove_contract_initialization(header: BlockHeader, contract_address: AztecAddress); -} - -impl ProveContractInitialization for BlockHeader { - fn prove_contract_initialization(self, contract_address: AztecAddress) { - // Compute initialization nullifier - let nullifier = compute_siloed_nullifier(contract_address, contract_address.to_field()); - - self.prove_nullifier_inclusion(nullifier); - } -} - -pub trait ProveContractNonInitialization { - fn prove_contract_non_initialization(header: BlockHeader, contract_address: AztecAddress); -} - -impl ProveContractNonInitialization for BlockHeader { - fn prove_contract_non_initialization(self, contract_address: AztecAddress) { - // Compute initialization nullifier - let nullifier = compute_siloed_nullifier(contract_address, contract_address.to_field()); - - self.prove_nullifier_non_inclusion(nullifier); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/history/deployment.nr b/noir-projects/aztec-nr/aztec/src/history/deployment.nr new file mode 100644 index 000000000000..4b2cf004e739 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/history/deployment.nr @@ -0,0 +1,56 @@ +//! Contract deployments. + +use dep::protocol_types::{ + abis::block_header::BlockHeader, address::AztecAddress, + constants::CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, hash::compute_siloed_nullifier, + traits::ToField, +}; + +use crate::history::nullifier::{assert_nullifier_did_not_exist_by, assert_nullifier_existed_by}; + +// This is tested in `noir-projects/noir-contracts/test_contract/src/test.nr because we cannot define a contract +// from within aztec.nr (due to the contract macro). + +pub fn assert_contract_bytecode_was_published_by( + block_header: BlockHeader, + contract_address: AztecAddress, +) { + let bytecode_publishing_nullifier = compute_siloed_nullifier( + CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, + contract_address.to_field(), + ); + + assert_nullifier_existed_by(block_header, bytecode_publishing_nullifier); +} + +pub fn assert_contract_bytecode_was_not_published_by( + block_header: BlockHeader, + contract_address: AztecAddress, +) { + let bytecode_publishing_nullifier = compute_siloed_nullifier( + CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, + contract_address.to_field(), + ); + + assert_nullifier_did_not_exist_by(block_header, bytecode_publishing_nullifier); +} + +pub fn assert_contract_was_initialized_by( + block_header: BlockHeader, + contract_address: AztecAddress, +) { + let initialization_nullifier = + compute_siloed_nullifier(contract_address, contract_address.to_field()); + + assert_nullifier_existed_by(block_header, initialization_nullifier); +} + +pub fn assert_contract_was_not_initialized_by( + block_header: BlockHeader, + contract_address: AztecAddress, +) { + let initialization_nullifier = + compute_siloed_nullifier(contract_address, contract_address.to_field()); + + assert_nullifier_did_not_exist_by(block_header, initialization_nullifier); +} diff --git a/noir-projects/aztec-nr/aztec/src/history/mod.nr b/noir-projects/aztec-nr/aztec/src/history/mod.nr index 027feb11c159..8070efbcf885 100644 --- a/noir-projects/aztec-nr/aztec/src/history/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/history/mod.nr @@ -1,9 +1,18 @@ //! Proofs of Aztec history. +//! +//! ## Data Availability +//! +//! Functions in these modules assert statements about the past state of the Aztec network. While this is possible in +//! theory, actual network nodes might not have the information required to produce proofs at relatively old blocks. +//! +//! In particular, many nodes prune old state and do not keep records of the different state trees (note hashes, +//! nullifiers, public storage, etc.) at blocks older than a couple hours, which are required in order to produce the +//! sibling paths these functions need. +//! +//! An archive node is therefore required when using any of these functions on non-recent blocks. -pub mod contract_inclusion; -pub mod note_inclusion; -pub mod note_validity; -pub mod nullifier_inclusion; -pub mod nullifier_non_inclusion; -pub mod public_storage; +pub mod deployment; +pub mod note; +pub mod nullifier; +pub mod storage; mod test; diff --git a/noir-projects/aztec-nr/aztec/src/history/note.nr b/noir-projects/aztec-nr/aztec/src/history/note.nr new file mode 100644 index 000000000000..11aae76e9f6a --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/history/note.nr @@ -0,0 +1,109 @@ +//! Note existence and non-nullification. + +use protocol_types::{ + abis::block_header::BlockHeader, + hash::{compute_siloed_note_hash, compute_siloed_nullifier, compute_unique_note_hash}, + merkle_tree::root::root_from_sibling_path, +}; + +use crate::{ + context::PrivateContext, + history::nullifier::{assert_nullifier_did_not_exist_by, assert_nullifier_existed_by}, + note::{ + ConfirmedNote, HintedNote, note_interface::NoteHash, + utils::compute_confirmed_note_hash_for_nullification, + }, + oracle::get_membership_witness::get_note_hash_membership_witness, +}; + +mod test; + +pub fn assert_note_existed_by( + block_header: BlockHeader, + hinted_note: HintedNote, +) -> ConfirmedNote +where + Note: NoteHash, +{ + let note_hash = hinted_note.note.compute_note_hash( + hinted_note.owner, + hinted_note.storage_slot, + hinted_note.randomness, + ); + + let siloed_note_hash = compute_siloed_note_hash(hinted_note.contract_address, note_hash); + + let unique_note_hash = compute_unique_note_hash( + hinted_note.metadata.to_settled().note_nonce(), + siloed_note_hash, + ); + + // Safety: The witness is only used as a "magical value" that makes the merkle proof below pass. Hence it's safe. + let witness = unsafe { get_note_hash_membership_witness(block_header, unique_note_hash) }; + + // Note inclusion is fairly straightforward, since all we need to prove is that a note exists in the note tree - + // we don't even care _where_ in the tree it is stored. This is because entries in the note hash tree are + // unique. + assert_eq( + block_header.state.partial.note_hash_tree.root, + root_from_sibling_path(unique_note_hash, witness.index, witness.path), + "Proving note inclusion failed", + ); + + ConfirmedNote::new(hinted_note, unique_note_hash) +} + +pub fn assert_note_was_valid_by( + block_header: BlockHeader, + hinted_note: HintedNote, + context: &mut PrivateContext, +) +where + Note: NoteHash, +{ + let confirmed_note = assert_note_existed_by(block_header, hinted_note); + assert_note_was_not_nullified_by(block_header, confirmed_note, context); +} + +pub fn assert_note_was_nullified_by( + block_header: BlockHeader, + confirmed_note: ConfirmedNote, + context: &mut PrivateContext, +) +where + Note: NoteHash, +{ + let note_hash_for_nullification = compute_confirmed_note_hash_for_nullification(confirmed_note); + let inner_nullifier = confirmed_note.note.compute_nullifier( + context, + confirmed_note.owner, + note_hash_for_nullification, + ); + + let siloed_nullifier = + compute_siloed_nullifier(confirmed_note.contract_address, inner_nullifier); + + assert_nullifier_existed_by(block_header, siloed_nullifier); +} + +pub fn assert_note_was_not_nullified_by( + block_header: BlockHeader, + confirmed_note: ConfirmedNote, + context: &mut PrivateContext, +) +where + Note: NoteHash, +{ + let note_hash_for_nullification = compute_confirmed_note_hash_for_nullification(confirmed_note); + + let inner_nullifier = confirmed_note.note.compute_nullifier( + context, + confirmed_note.owner, + note_hash_for_nullification, + ); + + let siloed_nullifier = + compute_siloed_nullifier(confirmed_note.contract_address, inner_nullifier); + + assert_nullifier_did_not_exist_by(block_header, siloed_nullifier); +} diff --git a/noir-projects/aztec-nr/aztec/src/history/note_validity/test.nr b/noir-projects/aztec-nr/aztec/src/history/note/test.nr similarity index 52% rename from noir-projects/aztec-nr/aztec/src/history/note_validity/test.nr rename to noir-projects/aztec-nr/aztec/src/history/note/test.nr index 2e7fa5c3f6ea..ccdd076be49a 100644 --- a/noir-projects/aztec-nr/aztec/src/history/note_validity/test.nr +++ b/noir-projects/aztec-nr/aztec/src/history/note/test.nr @@ -1,17 +1,42 @@ -use crate::history::note_validity::ProveNoteValidity; -use crate::history::test; +use crate::history::{note::{assert_note_existed_by, assert_note_was_valid_by}, test}; use crate::test::helpers::test_environment::PrivateContextOptions; +#[test] +unconstrained fn succeeds_on_blocks_after_note_creation() { + let (env, hinted_note) = test::create_note(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(test::NOTE_CREATED_AT), + |context| { + let header = context.anchor_block_header; + let _ = assert_note_existed_by(header, hinted_note); + }, + ); +} + #[test(should_fail_with = "not found in the note hash tree at block")] unconstrained fn fails_on_blocks_before_note_creation() { + let (env, hinted_note) = test::create_note(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(test::NOTE_CREATED_AT - 1), + |context| { + let header = context.anchor_block_header; + let _ = assert_note_existed_by(header, hinted_note); + }, + ); +} + +#[test(should_fail_with = "not found in the note hash tree at block")] +unconstrained fn validity_fails_on_blocks_before_note_creation() { let (env, hinted_note) = test::create_note_and_nullify_it(); env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(test::NOTE_CREATED_AT - 1), |context| { let header = context.anchor_block_header; - header.prove_note_validity(hinted_note, context); + assert_note_was_valid_by(header, hinted_note, context); }, ); } @@ -24,7 +49,7 @@ unconstrained fn succeeds_on_blocks_after_creation_and_before_nullification() { PrivateContextOptions::new().at_anchor_block_number(test::NOTE_CREATED_AT), |context| { let header = context.anchor_block_header; - header.prove_note_validity(hinted_note, context); + assert_note_was_valid_by(header, hinted_note, context); }, ); } @@ -37,7 +62,7 @@ unconstrained fn fails_on_blocks_after_note_nullification() { PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT), |context| { let header = context.anchor_block_header; - header.prove_note_validity(hinted_note, context); + assert_note_was_valid_by(header, hinted_note, context); }, ); } diff --git a/noir-projects/aztec-nr/aztec/src/history/note_inclusion.nr b/noir-projects/aztec-nr/aztec/src/history/note_inclusion.nr deleted file mode 100644 index 26890ac24c42..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/note_inclusion.nr +++ /dev/null @@ -1,55 +0,0 @@ -use protocol_types::{ - abis::block_header::BlockHeader, - hash::{compute_siloed_note_hash, compute_unique_note_hash}, - merkle_tree::root::root_from_sibling_path, -}; - -use crate::{ - note::{ConfirmedNote, HintedNote, note_interface::NoteHash}, - oracle::get_membership_witness::get_note_hash_membership_witness, -}; - -mod test; - -pub trait ProveNoteInclusion { - fn prove_note_inclusion( - header: BlockHeader, - hinted_note: HintedNote, - ) -> ConfirmedNote - where - Note: NoteHash; -} - -impl ProveNoteInclusion for BlockHeader { - fn prove_note_inclusion(self, hinted_note: HintedNote) -> ConfirmedNote - where - Note: NoteHash, - { - let note_hash = hinted_note.note.compute_note_hash( - hinted_note.owner, - hinted_note.storage_slot, - hinted_note.randomness, - ); - - let siloed_note_hash = compute_siloed_note_hash(hinted_note.contract_address, note_hash); - - let unique_note_hash = compute_unique_note_hash( - hinted_note.metadata.to_settled().note_nonce(), - siloed_note_hash, - ); - - // Safety: The witness is only used as a "magical value" that makes the merkle proof below pass. Hence it's safe. - let witness = unsafe { get_note_hash_membership_witness(self, unique_note_hash) }; - - // Note inclusion is fairly straightforward, since all we need to prove is that a note exists in the note tree - - // we don't even care _where_ in the tree it is stored. This is because entries in the note hash tree are - // unique. - assert_eq( - self.state.partial.note_hash_tree.root, - root_from_sibling_path(unique_note_hash, witness.index, witness.path), - "Proving note inclusion failed", - ); - - ConfirmedNote::new(hinted_note, unique_note_hash) - } -} diff --git a/noir-projects/aztec-nr/aztec/src/history/note_inclusion/test.nr b/noir-projects/aztec-nr/aztec/src/history/note_inclusion/test.nr deleted file mode 100644 index d8a1283899a6..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/note_inclusion/test.nr +++ /dev/null @@ -1,29 +0,0 @@ -use crate::history::{note_inclusion::ProveNoteInclusion, test}; - -use crate::test::helpers::test_environment::PrivateContextOptions; - -#[test] -unconstrained fn succeeds_on_blocks_after_note_creation() { - let (env, hinted_note) = test::create_note(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(test::NOTE_CREATED_AT), - |context| { - let header = context.anchor_block_header; - let _ = header.prove_note_inclusion(hinted_note); - }, - ); -} - -#[test(should_fail_with = "not found in the note hash tree at block")] -unconstrained fn fails_on_blocks_before_note_creation() { - let (env, hinted_note) = test::create_note(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(test::NOTE_CREATED_AT - 1), - |context| { - let header = context.anchor_block_header; - let _ = header.prove_note_inclusion(hinted_note); - }, - ); -} diff --git a/noir-projects/aztec-nr/aztec/src/history/note_validity.nr b/noir-projects/aztec-nr/aztec/src/history/note_validity.nr deleted file mode 100644 index 8202989420cb..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/note_validity.nr +++ /dev/null @@ -1,29 +0,0 @@ -use crate::{ - context::PrivateContext, - history::{note_inclusion::ProveNoteInclusion, nullifier_non_inclusion::ProveNoteNotNullified}, - note::{HintedNote, note_interface::NoteHash}, -}; - -use protocol_types::abis::block_header::BlockHeader; - -mod test; - -trait ProveNoteValidity { - fn prove_note_validity( - header: BlockHeader, - hinted_note: HintedNote, - context: &mut PrivateContext, - ) - where - Note: NoteHash; -} - -impl ProveNoteValidity for BlockHeader { - fn prove_note_validity(self, hinted_note: HintedNote, context: &mut PrivateContext) - where - Note: NoteHash, - { - let confirmed_note = self.prove_note_inclusion(hinted_note); - self.prove_note_not_nullified(confirmed_note, context); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier.nr new file mode 100644 index 000000000000..21ca867a6052 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/history/nullifier.nr @@ -0,0 +1,127 @@ +//! Nullifier existence and non-existence. + +use crate::oracle::get_nullifier_membership_witness::{ + get_low_nullifier_membership_witness, get_nullifier_membership_witness, +}; + +use protocol_types::{ + abis::block_header::BlockHeader, + merkle_tree::root::root_from_sibling_path, + traits::Hash, + utils::field::{full_field_greater_than, full_field_less_than}, +}; + +mod test; + +/// Asserts that a nullifier existed by the time a block was mined. +/// +/// This function takes a _siloed_ nullifier, i.e. the value that is actually stored in the tree. Use +/// [`crate::protocol_types::hash::compute_siloed_nullifier`] to convert an inner nullifier (what +/// [`PrivateContext::push_nullifier`](crate::context::PrivateContext::push_nullifier) takes) into a siloed one. +/// +/// Note that this does not mean that the nullifier was created **at** `block_header`, only that it was present in the +/// tree once all transactions from `block_header` were executed. +/// +/// In order to prove that a nullifier did **not** exist by `block_header`, use [`assert_nullifier_did_not_exist_by`]. +/// +/// ## Cost +/// +/// This function performs a full merkle tree inclusion proof, which is in the order of 4k gates. +/// +/// If you don't need to assert existence at a _specific_ past block, consider using +/// [`PrivateContext::assert_nullifier_exists`](crate::context::PrivateContext::assert_nullifier_exists) instead, which +/// is typically cheaper. Note that there are semantic differences though, as that function also considers _pending_ +/// nullifiers. +pub fn assert_nullifier_existed_by(block_header: BlockHeader, siloed_nullifier: Field) { + // 1) Get the membership witness of the nullifier + // Safety: The witness is only used as a "magical value" that makes the proof below pass. Hence it's safe. + let witness = unsafe { get_nullifier_membership_witness(block_header, siloed_nullifier) }; + + // 2) First we prove that the tree leaf in the witness is present in the nullifier tree. This is expected to be + // the leaf that contains the nullifier we're proving inclusion for. + assert_eq( + block_header.state.partial.nullifier_tree.root, + root_from_sibling_path(witness.leaf_preimage.hash(), witness.index, witness.path), + "Proving nullifier inclusion failed", + ); + + // 3) Then we simply check that the value in the leaf is the expected one. Note that we don't need to perform + // any checks on the rest of the values in the leaf preimage (the next index or next nullifier), since all we + // care about is showing that the tree contains an entry with the expected nullifier. + assert_eq( + witness.leaf_preimage.nullifier, + siloed_nullifier, + "Nullifier does not match value in witness", + ); +} + +/// Asserts that a nullifier did not exist by the time a block was mined. +/// +/// This function takes a _siloed_ nullifier, i.e. the value that is actually stored in the tree. Use +/// [`crate::protocol_types::hash::compute_siloed_nullifier`] to convert an inner nullifier (what +/// [`PrivateContext::push_nullifier`](crate::context::PrivateContext::push_nullifier) takes) into a siloed one. +/// +/// In order to prove that a nullifier **did** exist by `block_header`, use [`assert_nullifier_existed_by`]. +/// +/// ## Nullifier Non-Existence +/// +/// Proving nullifier non-existence is always nuanced, as it is not possible to privately prove that a nullifier does +/// not exist by the time a transaction is executed. What this function does instead is assert that once all +/// transactions from `block_header` were executed, the nullifier was not in the tree. +/// +/// If you **must** prove that a nullifier does not exist by the time a transaction is executed, there only two ways to +/// do this: by actually emitting the nullifier via +/// [`PrivateContext::push_nullifier`](crate::context::PrivateContext::push_nullifier) (which can of course can be done +/// once), or by calling a public contract function that calls +/// [`PublicContext::nullifier_exists_unsafe`](crate::context::PublicContext::nullifier_exists_unsafe) (which leaks that +/// this nullifier is being checked): +/// +/// ```noir +/// #[external("public")] +/// #[only_self] +/// fn _assert_nullifier_does_not_exist(unsileod_nullifier: Field, contract_address: AztecAddress) { +/// assert(!self.context.nullifier_exists_unsafe(unsiloed_nullifier, contract_address)); +/// } +/// ``` +/// +/// ## Cost +/// +/// This function performs a full merkle tree inclusion proof, which is in the order of 4k gates. +pub fn assert_nullifier_did_not_exist_by(block_header: BlockHeader, siloed_nullifier: Field) { + // 1) Get the membership witness of a low nullifier of the nullifier + // Safety: The witness is only used as a "magical value" that makes the proof below pass. Hence it's safe. + let witness = unsafe { get_low_nullifier_membership_witness(block_header, siloed_nullifier) }; + + // 2) Check that the leaf preimage is not empty. + // An empty leaf preimage would pass validation as a low leaf. However, it's not a valid low leaf. It's used to + // pad the nullifiers emitted from a tx so they can be inserted into the tree in a fixed-size batch. + assert( + !witness.leaf_preimage.is_empty(), + "The provided nullifier tree leaf preimage cannot be empty", + ); + + // 3) Prove that the tree leaf in the witness is present in the nullifier tree. This is expected to be + // the 'low leaf', i.e. the leaf that would come immediately before the nullifier's leaf, if the nullifier were + // to be in the tree. + let low_nullifier_leaf = witness.leaf_preimage; + assert_eq( + block_header.state.partial.nullifier_tree.root, + root_from_sibling_path(low_nullifier_leaf.hash(), witness.index, witness.path), + "Proving nullifier non-inclusion failed: Could not prove low nullifier inclusion", + ); + + // 4) Prove that the low leaf is indeed smaller than the nullifier + assert( + full_field_less_than(low_nullifier_leaf.nullifier, siloed_nullifier), + "Proving nullifier non-inclusion failed: low_nullifier.value < nullifier.value check failed", + ); + + // 5) Prove that the low leaf is pointing "over" the nullifier, which means that the nullifier is not included + // in the nullifier tree, since if it were it'd need to be between the low leaf and the next leaf. Note the + // special case in which the low leaf is the largest of all entries, in which case there's no 'next' entry. + assert( + full_field_greater_than(low_nullifier_leaf.next_nullifier, siloed_nullifier) + | (low_nullifier_leaf.next_index == 0), + "Proving nullifier non-inclusion failed: low_nullifier.next_value > nullifier.value check failed", + ); +} diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier/test.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier/test.nr new file mode 100644 index 000000000000..785b19fa9df1 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/history/nullifier/test.nr @@ -0,0 +1,138 @@ +use crate::history::{ + note::{assert_note_existed_by, assert_note_was_not_nullified_by, assert_note_was_nullified_by}, + nullifier::{assert_nullifier_did_not_exist_by, assert_nullifier_existed_by}, + test, +}; +use crate::oracle::{nullifiers::notify_created_nullifier, random::random}; +use crate::test::helpers::test_environment::{PrivateContextOptions, TestEnvironment}; +use dep::protocol_types::{ + constants::DOM_SEP__OUTER_NULLIFIER, hash::poseidon2_hash_with_separator, traits::ToField, +}; + +#[test] +unconstrained fn note_is_nullified_succeeds_on_blocks_after_note_nullification() { + let (env, hinted_note) = test::create_note_and_nullify_it(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT), + |context| { + let header = context.anchor_block_header; + let confirmed_note = assert_note_existed_by(header, hinted_note); + assert_note_was_nullified_by(header, confirmed_note, context); + }, + ); +} + +#[test(should_fail_with = "Nullifier membership witness not found at block")] +unconstrained fn note_is_nullified_fails_on_blocks_before_note_nullification() { + let (env, hinted_note) = test::create_note_and_nullify_it(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT - 1), + |context| { + let header = context.anchor_block_header; + let confirmed_note = assert_note_existed_by(header, hinted_note); + assert_note_was_nullified_by(header, confirmed_note, context); + }, + ); +} + +#[test] +unconstrained fn assert_nullifier_existed_by_succeeds_on_blocks_after_nullifier_creation() { + let env = TestEnvironment::new(); + + let siloed_nullifier = env.private_context(|context| { + let unsiloed_nullifier = 42069; + notify_created_nullifier(unsiloed_nullifier); + + // We need to compute the siloed nullifier so we can check for its inclusion in state. + poseidon2_hash_with_separator( + [context.this_address().to_field(), unsiloed_nullifier], + DOM_SEP__OUTER_NULLIFIER, + ) + }); + + let nullifier_created_at = env.last_block_number(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(nullifier_created_at), + |context| { + let header = context.anchor_block_header; + assert_nullifier_existed_by(header, siloed_nullifier); + }, + ); +} + +#[test(should_fail_with = "Nullifier membership witness not found")] +unconstrained fn assert_nullifier_existed_by_fails_on_blocks_before_nullifier_creation() { + let env = TestEnvironment::new(); + + env.private_context(|context| { + let header = context.anchor_block_header; + assert_nullifier_existed_by(header, random()); + }); +} + +#[test] +unconstrained fn note_not_nullified_succeeds_in_blocks_before_note_nullification() { + let (env, hinted_note) = test::create_note_and_nullify_it(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT - 1), + |context| { + let header = context.anchor_block_header; + let confirmed_note = assert_note_existed_by(header, hinted_note); + assert_note_was_not_nullified_by(header, confirmed_note, context); + }, + ); +} + +#[test(should_fail_with = "Proving nullifier non-inclusion failed")] +unconstrained fn note_not_nullified_fails_in_blocks_after_note_nullification_fails() { + let (env, hinted_note) = test::create_note_and_nullify_it(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT), + |context| { + let header = context.anchor_block_header; + let confirmed_note = assert_note_existed_by(header, hinted_note); + assert_note_was_not_nullified_by(header, confirmed_note, context); + }, + ); +} + +#[test] +unconstrained fn nullifier_non_inclusion_succeeds_in_blocks_before_nullifier_creation() { + let env = TestEnvironment::new(); + + env.private_context(|context| { + let header = context.anchor_block_header; + assert_nullifier_did_not_exist_by(header, random()); + }); +} + +#[test(should_fail_with = "Proving nullifier non-inclusion failed")] +unconstrained fn nullifier_non_inclusion_fails_in_blocks_after_nullifier_creation() { + let env = TestEnvironment::new(); + + let siloed_nullifier = env.private_context(|context| { + let unsiloed_nullifier = 42069; + notify_created_nullifier(unsiloed_nullifier); + + // We need to compute the siloed nullifier so we can check for its inclusion in state. + poseidon2_hash_with_separator( + [context.this_address().to_field(), unsiloed_nullifier], + DOM_SEP__OUTER_NULLIFIER, + ) + }); + + let nullifier_created_at = env.last_block_number(); + + env.private_context_opts( + PrivateContextOptions::new().at_anchor_block_number(nullifier_created_at), + |context| { + let header = context.anchor_block_header; + assert_nullifier_did_not_exist_by(header, siloed_nullifier); + }, + ); +} diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr deleted file mode 100644 index 3a3b9dc4677c..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion.nr +++ /dev/null @@ -1,78 +0,0 @@ -use crate::{ - context::PrivateContext, - note::{ - ConfirmedNote, note_interface::NoteHash, - utils::compute_confirmed_note_hash_for_nullification, - }, - oracle::get_nullifier_membership_witness::get_nullifier_membership_witness, -}; - -use protocol_types::{ - abis::block_header::BlockHeader, hash::compute_siloed_nullifier, - merkle_tree::root::root_from_sibling_path, traits::Hash, -}; - -mod test; - -pub trait ProveNullifierInclusion { - fn prove_nullifier_inclusion(header: BlockHeader, nullifier: Field); -} - -impl ProveNullifierInclusion for BlockHeader { - fn prove_nullifier_inclusion(self, nullifier: Field) { - // 1) Get the membership witness of the nullifier - // Safety: The witness is only used as a "magical value" that makes the proof below pass. Hence it's safe. - let witness = unsafe { get_nullifier_membership_witness(self, nullifier) }; - - // 2) First we prove that the tree leaf in the witness is present in the nullifier tree. This is expected to be - // the leaf that contains the nullifier we're proving inclusion for. - assert_eq( - self.state.partial.nullifier_tree.root, - root_from_sibling_path(witness.leaf_preimage.hash(), witness.index, witness.path), - "Proving nullifier inclusion failed", - ); - - // 3) Then we simply check that the value in the leaf is the expected one. Note that we don't need to perform - // any checks on the rest of the values in the leaf preimage (the next index or next nullifier), since all we - // care about is showing that the tree contains an entry with the expected nullifier. - assert_eq( - witness.leaf_preimage.nullifier, - nullifier, - "Nullifier does not match value in witness", - ); - } -} - -pub trait ProveNoteIsNullified { - fn prove_note_is_nullified( - header: BlockHeader, - confirmed_note: ConfirmedNote, - context: &mut PrivateContext, - ) - where - Note: NoteHash; -} - -impl ProveNoteIsNullified for BlockHeader { - fn prove_note_is_nullified( - self, - confirmed_note: ConfirmedNote, - context: &mut PrivateContext, - ) - where - Note: NoteHash, - { - let note_hash_for_nullification = - compute_confirmed_note_hash_for_nullification(confirmed_note); - let inner_nullifier = confirmed_note.note.compute_nullifier( - context, - confirmed_note.owner, - note_hash_for_nullification, - ); - - let siloed_nullifier = - compute_siloed_nullifier(confirmed_note.contract_address, inner_nullifier); - - self.prove_nullifier_inclusion(siloed_nullifier); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion/test.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion/test.nr deleted file mode 100644 index 6f3e65c0e713..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/nullifier_inclusion/test.nr +++ /dev/null @@ -1,74 +0,0 @@ -use crate::history::{ - note_inclusion::ProveNoteInclusion, - nullifier_inclusion::{ProveNoteIsNullified, ProveNullifierInclusion}, - test, -}; -use crate::oracle::{nullifiers::notify_created_nullifier, random::random}; -use crate::test::helpers::test_environment::{PrivateContextOptions, TestEnvironment}; -use dep::protocol_types::{ - constants::DOM_SEP__OUTER_NULLIFIER, hash::poseidon2_hash_with_separator, traits::ToField, -}; - -#[test] -unconstrained fn note_is_nullified_succeeds_on_blocks_after_note_nullification() { - let (env, hinted_note) = test::create_note_and_nullify_it(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT), - |context| { - let header = context.anchor_block_header; - let confirmed_note = header.prove_note_inclusion(hinted_note); - header.prove_note_is_nullified(confirmed_note, context); - }, - ); -} - -#[test(should_fail_with = "Nullifier membership witness not found at block")] -unconstrained fn note_is_nullified_fails_on_blocks_before_note_nullification() { - let (env, hinted_note) = test::create_note_and_nullify_it(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT - 1), - |context| { - let header = context.anchor_block_header; - let confirmed_note = header.prove_note_inclusion(hinted_note); - header.prove_note_is_nullified(confirmed_note, context); - }, - ); -} - -#[test] -unconstrained fn prove_nullifier_inclusion_succeeds_on_blocks_after_nullifier_creation() { - let env = TestEnvironment::new(); - - let siloed_nullifier = env.private_context(|context| { - let unsiloed_nullifier = 42069; - notify_created_nullifier(unsiloed_nullifier); - - // We need to compute the siloed nullifier so we can check for its inclusion in state. - poseidon2_hash_with_separator( - [context.this_address().to_field(), unsiloed_nullifier], - DOM_SEP__OUTER_NULLIFIER, - ) - }); - - let nullifier_created_at = env.last_block_number(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(nullifier_created_at), - |context| { - let header = context.anchor_block_header; - header.prove_nullifier_inclusion(siloed_nullifier); - }, - ); -} - -#[test(should_fail_with = "Nullifier membership witness not found")] -unconstrained fn prove_nullifier_inclusion_fails_on_blocks_before_nullifier_creation() { - let env = TestEnvironment::new(); - - env.private_context(|context| { - let header = context.anchor_block_header; - header.prove_nullifier_inclusion(random()); - }); -} diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr deleted file mode 100644 index 6462b3c8fdb5..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion.nr +++ /dev/null @@ -1,97 +0,0 @@ -use crate::{ - context::PrivateContext, - note::{ - ConfirmedNote, note_interface::NoteHash, - utils::compute_confirmed_note_hash_for_nullification, - }, - oracle::get_nullifier_membership_witness::get_low_nullifier_membership_witness, -}; -use protocol_types::{ - abis::block_header::BlockHeader, - hash::compute_siloed_nullifier, - merkle_tree::root::root_from_sibling_path, - traits::Hash, - utils::field::{full_field_greater_than, full_field_less_than}, -}; - -mod test; - -pub trait ProveNullifierNonInclusion { - fn prove_nullifier_non_inclusion(header: BlockHeader, nullifier: Field); -} - -impl ProveNullifierNonInclusion for BlockHeader { - fn prove_nullifier_non_inclusion(self, nullifier: Field) { - // 1) Get the membership witness of a low nullifier of the nullifier - // Safety: The witness is only used as a "magical value" that makes the proof below pass. Hence it's safe. - let witness = unsafe { get_low_nullifier_membership_witness(self, nullifier) }; - - // 2) Check that the leaf preimage is not empty. - // An empty leaf preimage would pass validation as a low leaf. However, it's not a valid low leaf. It's used to - // pad the nullifiers emitted from a tx so they can be inserted into the tree in a fixed-size batch. - assert( - !witness.leaf_preimage.is_empty(), - "The provided public data tree leaf preimage cannot be empty", - ); - - // 3) Prove that the tree leaf in the witness is present in the nullifier tree. This is expected to be - // the 'low leaf', i.e. the leaf that would come immediately before the nullifier's leaf, if the nullifier were - // to be in the tree. - let low_nullifier_leaf = witness.leaf_preimage; - assert_eq( - self.state.partial.nullifier_tree.root, - root_from_sibling_path(low_nullifier_leaf.hash(), witness.index, witness.path), - "Proving nullifier non-inclusion failed: Could not prove low nullifier inclusion", - ); - - // 4) Prove that the low leaf is indeed smaller than the nullifier - assert( - full_field_less_than(low_nullifier_leaf.nullifier, nullifier), - "Proving nullifier non-inclusion failed: low_nullifier.value < nullifier.value check failed", - ); - - // 5) Prove that the low leaf is pointing "over" the nullifier, which means that the nullifier is not included - // in the nullifier tree, since if it were it'd need to be between the low leaf and the next leaf. Note the - // special case in which the low leaf is the largest of all entries, in which case there's no 'next' entry. - assert( - full_field_greater_than(low_nullifier_leaf.next_nullifier, nullifier) - | (low_nullifier_leaf.next_index == 0), - "Proving nullifier non-inclusion failed: low_nullifier.next_value > nullifier.value check failed", - ); - } -} - -pub trait ProveNoteNotNullified { - fn prove_note_not_nullified( - header: BlockHeader, - confirmed_note: ConfirmedNote, - context: &mut PrivateContext, - ) - where - Note: NoteHash; -} - -impl ProveNoteNotNullified for BlockHeader { - fn prove_note_not_nullified( - self, - confirmed_note: ConfirmedNote, - context: &mut PrivateContext, - ) - where - Note: NoteHash, - { - let note_hash_for_nullification = - compute_confirmed_note_hash_for_nullification(confirmed_note); - - let inner_nullifier = confirmed_note.note.compute_nullifier( - context, - confirmed_note.owner, - note_hash_for_nullification, - ); - - let siloed_nullifier = - compute_siloed_nullifier(confirmed_note.contract_address, inner_nullifier); - - self.prove_nullifier_non_inclusion(siloed_nullifier); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion/test.nr b/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion/test.nr deleted file mode 100644 index 2cc531973a70..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/nullifier_non_inclusion/test.nr +++ /dev/null @@ -1,74 +0,0 @@ -use crate::history::{ - note_inclusion::ProveNoteInclusion, - nullifier_non_inclusion::{ProveNoteNotNullified, ProveNullifierNonInclusion}, - test, -}; -use crate::oracle::{nullifiers::notify_created_nullifier, random::random}; -use crate::test::helpers::test_environment::{PrivateContextOptions, TestEnvironment}; -use dep::protocol_types::{ - constants::DOM_SEP__OUTER_NULLIFIER, hash::poseidon2_hash_with_separator, traits::ToField, -}; - -#[test] -unconstrained fn note_not_nullified_succeeds_in_blocks_before_note_nullification() { - let (env, hinted_note) = test::create_note_and_nullify_it(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT - 1), - |context| { - let header = context.anchor_block_header; - let confirmed_note = header.prove_note_inclusion(hinted_note); - header.prove_note_not_nullified(confirmed_note, context); - }, - ); -} - -#[test(should_fail_with = "Proving nullifier non-inclusion failed")] -unconstrained fn note_not_nullified_fails_in_blocks_after_note_nullification_fails() { - let (env, hinted_note) = test::create_note_and_nullify_it(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(test::NOTE_NULLIFIED_AT), - |context| { - let header = context.anchor_block_header; - let confirmed_note = header.prove_note_inclusion(hinted_note); - header.prove_note_not_nullified(confirmed_note, context); - }, - ); -} - -#[test] -unconstrained fn nullifier_non_inclusion_succeeds_in_blocks_before_nullifier_creation() { - let env = TestEnvironment::new(); - - env.private_context(|context| { - let header = context.anchor_block_header; - header.prove_nullifier_non_inclusion(random()); - }); -} - -#[test(should_fail_with = "Proving nullifier non-inclusion failed")] -unconstrained fn nullifier_non_inclusion_fails_in_blocks_after_nullifier_creation() { - let env = TestEnvironment::new(); - - let siloed_nullifier = env.private_context(|context| { - let unsiloed_nullifier = 42069; - notify_created_nullifier(unsiloed_nullifier); - - // We need to compute the siloed nullifier so we can check for its inclusion in state. - poseidon2_hash_with_separator( - [context.this_address().to_field(), unsiloed_nullifier], - DOM_SEP__OUTER_NULLIFIER, - ) - }); - - let nullifier_created_at = env.last_block_number(); - - env.private_context_opts( - PrivateContextOptions::new().at_anchor_block_number(nullifier_created_at), - |context| { - let header = context.anchor_block_header; - header.prove_nullifier_non_inclusion(siloed_nullifier); - }, - ); -} diff --git a/noir-projects/aztec-nr/aztec/src/history/public_storage.nr b/noir-projects/aztec-nr/aztec/src/history/public_storage.nr deleted file mode 100644 index 57ef0f2ad351..000000000000 --- a/noir-projects/aztec-nr/aztec/src/history/public_storage.nr +++ /dev/null @@ -1,42 +0,0 @@ -use dep::protocol_types::{ - abis::block_header::BlockHeader, address::AztecAddress, constants::DOM_SEP__PUBLIC_LEAF_INDEX, - data::public_data_storage_read, hash::poseidon2_hash_with_separator, - merkle_tree::MembershipWitness, traits::ToField, -}; - -use crate::oracle::get_public_data_witness::get_public_data_witness; - -mod test; - -pub trait PublicStorageHistoricalRead { - fn public_storage_historical_read( - header: BlockHeader, - storage_slot: Field, - contract_address: AztecAddress, - ) -> Field; -} - -impl PublicStorageHistoricalRead for BlockHeader { - fn public_storage_historical_read( - self, - storage_slot: Field, - contract_address: AztecAddress, - ) -> Field { - // 1) Compute the leaf index by siloing the storage slot with the contract address - let public_data_tree_index = poseidon2_hash_with_separator( - [contract_address.to_field(), storage_slot], - DOM_SEP__PUBLIC_LEAF_INDEX, - ); - - // 2) Get the membership witness for the tree index. - // Safety: The witness is only used as a "magical value" that makes the proof below pass. Hence it's safe. - let witness = unsafe { get_public_data_witness(self, public_data_tree_index) }; - - public_data_storage_read( - self.state.partial.public_data_tree.root, - public_data_tree_index, - MembershipWitness { leaf_index: witness.index, sibling_path: witness.path }, - witness.leaf_preimage, - ) - } -} diff --git a/noir-projects/aztec-nr/aztec/src/history/storage.nr b/noir-projects/aztec-nr/aztec/src/history/storage.nr new file mode 100644 index 000000000000..71f1ece50542 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/history/storage.nr @@ -0,0 +1,34 @@ +//! Historical storage accesses. + +use dep::protocol_types::{ + abis::block_header::BlockHeader, address::AztecAddress, constants::DOM_SEP__PUBLIC_LEAF_INDEX, + data::public_data_storage_read, hash::poseidon2_hash_with_separator, + merkle_tree::MembershipWitness, traits::ToField, +}; + +use crate::oracle::get_public_data_witness::get_public_data_witness; + +mod test; + +pub fn public_storage_historical_read( + block_header: BlockHeader, + storage_slot: Field, + contract_address: AztecAddress, +) -> Field { + // 1) Compute the leaf index by siloing the storage slot with the contract address + let public_data_tree_index = poseidon2_hash_with_separator( + [contract_address.to_field(), storage_slot], + DOM_SEP__PUBLIC_LEAF_INDEX, + ); + + // 2) Get the membership witness for the tree index. + // Safety: The witness is only used as a "magical value" that makes the proof below pass. Hence it's safe. + let witness = unsafe { get_public_data_witness(block_header, public_data_tree_index) }; + + public_data_storage_read( + block_header.state.partial.public_data_tree.root, + public_data_tree_index, + MembershipWitness { leaf_index: witness.index, sibling_path: witness.path }, + witness.leaf_preimage, + ) +} diff --git a/noir-projects/aztec-nr/aztec/src/history/public_storage/test.nr b/noir-projects/aztec-nr/aztec/src/history/storage/test.nr similarity index 81% rename from noir-projects/aztec-nr/aztec/src/history/public_storage/test.nr rename to noir-projects/aztec-nr/aztec/src/history/storage/test.nr index 8f495f9e0201..5066cbda7d86 100644 --- a/noir-projects/aztec-nr/aztec/src/history/public_storage/test.nr +++ b/noir-projects/aztec-nr/aztec/src/history/storage/test.nr @@ -1,5 +1,5 @@ use crate::{ - history::public_storage::PublicStorageHistoricalRead, + history::storage::public_storage_historical_read, test::helpers::test_environment::{PrivateContextOptions, TestEnvironment}, }; @@ -12,7 +12,8 @@ unconstrained fn historical_no_write_public_storage_read() { env.private_context(|context| { assert_eq( - context.anchor_block_header.public_storage_historical_read( + public_storage_historical_read( + context.anchor_block_header, STORAGE_SLOT, context.this_address(), ), @@ -33,7 +34,8 @@ unconstrained fn historical_pre_write_public_storage_read() { PrivateContextOptions::new().at_anchor_block_number(write_block_number - 1), |context| { assert_eq( - context.anchor_block_header.public_storage_historical_read( + public_storage_historical_read( + context.anchor_block_header, STORAGE_SLOT, context.this_address(), ), @@ -51,7 +53,8 @@ unconstrained fn historical_post_write_public_storage_read() { env.private_context(|context| { assert_eq( - context.anchor_block_header.public_storage_historical_read( + public_storage_historical_read( + context.anchor_block_header, STORAGE_SLOT, context.this_address(), ), diff --git a/noir-projects/aztec-nr/aztec/src/note/confirmed_note.nr b/noir-projects/aztec-nr/aztec/src/note/confirmed_note.nr index 1cc86fc76d88..522336d54fb7 100644 --- a/noir-projects/aztec-nr/aztec/src/note/confirmed_note.nr +++ b/noir-projects/aztec-nr/aztec/src/note/confirmed_note.nr @@ -11,7 +11,7 @@ use protocol_types::address::AztecAddress; /// /// `ConfirmedNote`s can be obtained by reading notes from PXE via [crate::note::note_getter::get_note] and /// [crate::note::note_getter::get_notes], or by confirming a [crate::note::hinted_note::HintedNote] via -/// [crate::history::note_inclusion::ProveNoteInclusion::prove_note_inclusion]. +/// [crate::history::note::assert_note_existed_by]. /// /// ## Pending Notes /// diff --git a/noir-projects/aztec-nr/aztec/src/note/hinted_note.nr b/noir-projects/aztec-nr/aztec/src/note/hinted_note.nr index 7084282e77b2..c9a685b35026 100644 --- a/noir-projects/aztec-nr/aztec/src/note/hinted_note.nr +++ b/noir-projects/aztec-nr/aztec/src/note/hinted_note.nr @@ -8,7 +8,7 @@ use protocol_types::{address::AztecAddress, traits::{Deserialize, Packable, Seri /// /// This value typically unconstrained (originating from oracles or as a contract function parameter), and can be /// converted into a [crate::note::confirmed_note::ConfirmedNote] via -/// [crate::history::note_inclusion::ProveNoteInclusion::prove_note_inclusion]. +/// [crate::history::note::assert_note_existed_by]. #[derive(Deserialize, Eq, Serialize, Packable)] pub struct HintedNote { pub note: Note, diff --git a/noir-projects/aztec-nr/aztec/src/note/mod.nr b/noir-projects/aztec-nr/aztec/src/note/mod.nr index d8fd8d1d8ee2..360af4800d11 100644 --- a/noir-projects/aztec-nr/aztec/src/note/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/mod.nr @@ -1,4 +1,7 @@ //! Note traits and utilities. +//! +//! ## Siloing +//! pub mod constants; pub mod lifecycle; diff --git a/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr b/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr index 429801328556..0a7585b145f8 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/with_hash.nr @@ -1,6 +1,6 @@ use crate::{ context::{PublicContext, UtilityContext}, - history::public_storage::PublicStorageHistoricalRead, + history::storage::public_storage_historical_read, oracle, }; use dep::protocol_types::{ @@ -74,7 +74,7 @@ where // The actual `value` (of type T, of packed length M fields) is stored in contiguous fields from the `storage_slot`. // The _hash_ of the `value` is stored at the end, at slot: `storage_slot + M`. let hash = - header_to_read_from.public_storage_historical_read(storage_slot + M as Field, address); + public_storage_historical_read(header_to_read_from, storage_slot + M as Field, address); if hash != 0 { assert_eq(hash, hint.get_hash(), "Hint values do not match hash"); diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index 9859a66ed947..db0b58dc6f7f 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -1,6 +1,6 @@ use dep::aztec::{ context::{PrivateContext, PublicContext}, - history::nullifier_inclusion::ProveNullifierInclusion, + history::nullifier::assert_nullifier_existed_by, keys::getters::{get_nsk_app, get_public_keys}, macros::notes::custom_note, messages::logs::partial_note::compute_partial_note_private_content_log, @@ -236,12 +236,15 @@ impl PartialUintNote { // We verify that the partial note we're completing is valid (i.e. completer is correct, it uses the correct // state variable's storage slot, and it is internally consistent). let validity_commitment = self.compute_validity_commitment(completer); - // `prove_nullifier_inclusion` function expects the nullifier to be siloed (hashed with the address of + // `assert_nullifier_existed_by` function expects the nullifier to be siloed (hashed with the address of // the contract that emitted the nullifier) as it checks the value directly against the nullifier tree and all // the nullifiers in the tree are siloed by the protocol. let siloed_validity_commitment = compute_siloed_nullifier(context.this_address(), validity_commitment); - context.get_anchor_block_header().prove_nullifier_inclusion(siloed_validity_commitment); + assert_nullifier_existed_by( + context.get_anchor_block_header(), + siloed_validity_commitment, + ); // We need to do two things: // - emit an unencrypted log containing the public fields (the value) via the private log channel. The diff --git a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr index a7e1d963e554..523706c476af 100644 --- a/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/claim_contract/src/main.nr @@ -3,7 +3,7 @@ use dep::aztec::macros::aztec; #[aztec] pub contract Claim { // docs:start:history_import - use dep::aztec::history::note_inclusion::ProveNoteInclusion; + use dep::aztec::history::note::assert_note_existed_by; // docs:end:history_import use dep::aztec::{ macros::{functions::{external, initializer}, storage::storage}, @@ -48,7 +48,7 @@ pub contract Claim { // 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 _ = header.prove_note_inclusion(hinted_note); + let _ = 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 @@ -57,7 +57,7 @@ pub contract Claim { // 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 prove_note_inclusion return note_hash_for_nullification. + // handle it on its own or we could make assert_note_existed_by return note_hash_for_nullification. let note_hash_for_nullification = compute_note_hash_for_nullification(hinted_note); let nullifier = hinted_note.note.compute_nullifier( self.context, diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr index 762dbf4527be..9c47442d5021 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/main.nr @@ -37,7 +37,7 @@ pub contract Test { // Event related messages::message_delivery::MessageDelivery, // History and inclusion proofs - history::note_inclusion::ProveNoteInclusion, + history::note::assert_note_existed_by, // Key management keys::getters::get_public_keys, // Macros @@ -261,7 +261,7 @@ pub contract Test { }; let header = self.context.get_anchor_block_header(); - let _ = header.prove_note_inclusion(hinted_note); + let _ = assert_note_existed_by(header, hinted_note); } #[external("private")] diff --git a/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr b/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr index 701d441bb99a..f03154a44e4e 100644 --- a/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr +++ b/noir-projects/noir-contracts/contracts/test/test_contract/src/test/deployment_proofs.nr @@ -1,14 +1,14 @@ use crate::Test; use dep::aztec::{ - history::contract_inclusion::{ - ProveContractDeployment, ProveContractInitialization, ProveContractNonDeployment, - ProveContractNonInitialization, + history::deployment::{ + assert_contract_bytecode_was_not_published_by, assert_contract_bytecode_was_published_by, + assert_contract_was_initialized_by, assert_contract_was_not_initialized_by, }, protocol_types::address::AztecAddress, test::helpers::test_environment::{PrivateContextOptions, TestEnvironment}, }; -// These tests need to be here instead of a saner place like aztec-nr/aztec/src/history/contract_inclusion/test.nr +// These tests need to be here instead of a saner place like aztec-nr/aztec/src/history/deployment/test.nr // because we are unable to define nor deploy a contract from that location and currently do not have plans to change // that. @@ -41,69 +41,93 @@ unconstrained fn contract_historical_proofs_happy_path() { env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_DEPLOYED_AT - 1), - |context| { context.anchor_block_header.prove_contract_non_deployment(contract_address); }, + |context| { + assert_contract_bytecode_was_not_published_by( + context.anchor_block_header, + contract_address, + ); + }, ); env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_DEPLOYED_AT), - |context| { context.anchor_block_header.prove_contract_deployment(contract_address); }, + |context| { + assert_contract_bytecode_was_published_by( + context.anchor_block_header, + contract_address, + ); + }, ); env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT - 1), |context| { - context.anchor_block_header.prove_contract_non_initialization(contract_address); + assert_contract_was_not_initialized_by(context.anchor_block_header, contract_address); }, ); env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT), - |context| { context.anchor_block_header.prove_contract_initialization(contract_address); }, + |context| { + assert_contract_was_initialized_by(context.anchor_block_header, contract_address); + }, ); } #[test(should_fail_with = "Nullifier membership witness not found at block")] -unconstrained fn prove_contract_deployment_before_deployment_fails() { +unconstrained fn assert_contract_bytecode_was_published_by_before_deployment_fails() { let (env, contract_address, _owner) = setup(); // Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_DEPLOYED_AT - 1), - |context| { context.anchor_block_header.prove_contract_deployment(contract_address); }, + |context| { + assert_contract_bytecode_was_published_by( + context.anchor_block_header, + contract_address, + ); + }, ); } #[test(should_fail_with = "Nullifier membership witness not found at block")] -unconstrained fn prove_contract_initialization_before_initialization_fails() { +unconstrained fn assert_contract_was_initialized_by_before_initialization_fails() { let (env, contract_address, _owner) = setup(); // Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT - 1), - |context| { context.anchor_block_header.prove_contract_initialization(contract_address); }, + |context| { + assert_contract_was_initialized_by(context.anchor_block_header, contract_address); + }, ); } #[test(should_fail_with = "Proving nullifier non-inclusion failed")] -unconstrained fn prove_contract_non_deployment_of_deployed_fails() { +unconstrained fn assert_contract_bytecode_was_not_published_by_of_deployed_fails() { let (env, contract_address, _owner) = setup(); // Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_DEPLOYED_AT), - |context| { context.anchor_block_header.prove_contract_non_deployment(contract_address); }, + |context| { + assert_contract_bytecode_was_not_published_by( + context.anchor_block_header, + contract_address, + ); + }, ); } #[test(should_fail_with = "Proving nullifier non-inclusion failed")] -unconstrained fn prove_contract_non_initialization_of_initialized_fails() { +unconstrained fn assert_contract_was_not_initialized_by_of_initialized_fails() { let (env, contract_address, _owner) = setup(); // Note that we're only testing that the function fails, but not that it would correct reject bad hints from an oracle env.private_context_opts( PrivateContextOptions::new().at_anchor_block_number(CONTRACT_INITIALIZED_AT), |context| { - context.anchor_block_header.prove_contract_non_initialization(contract_address); + assert_contract_was_not_initialized_by(context.anchor_block_header, contract_address); }, ); }