From 42a4b1c6f000886b8b63e2fd6b0b218a29cb820c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Tue, 11 Jun 2024 20:01:50 +0100 Subject: [PATCH] feat: `pxe.addNullifiedNote(...)` (#6948) --- boxes/boxes/react/src/config.ts | 2 +- .../aztecnr-getting-started.md | 2 +- .../writing_contracts/how_to_emit_event.md | 2 +- .../src/core/libraries/ConstantsGen.sol | 8 +- .../aztec-nr/aztec/src/note/utils.nr | 17 ++- noir-projects/aztec-nr/aztec/src/prelude.nr | 2 +- .../crates/types/src/constants.nr | 8 +- noir/noir-repo/aztec_macros/src/lib.rs | 4 +- ...e_note_hash_and_optionally_a_nullifier.rs} | 54 ++++---- .../aztec_macros/src/transforms/mod.rs | 2 +- .../aztec_macros/src/utils/errors.rs | 6 +- .../noirc_frontend/src/hir_def/types.rs | 4 + .../aztec.js/src/wallet/base_wallet.ts | 3 + .../circuit-types/src/interfaces/pxe.ts | 10 ++ yarn-project/circuits.js/src/constants.gen.ts | 8 +- yarn-project/cli/src/cmds/inspect_contract.ts | 2 +- .../end-to-end/src/e2e_2_pxes.test.ts | 121 +++++++++++------- .../pxe/src/database/incoming_note_dao.ts | 5 +- .../pxe/src/database/kv_pxe_database.ts | 12 ++ yarn-project/pxe/src/database/pxe_database.ts | 6 + .../src/note_processor/note_processor.test.ts | 4 +- .../src/note_processor/produce_note_dao.ts | 3 +- .../pxe/src/pxe_service/pxe_service.ts | 70 ++++++++-- .../src/client/private_execution.test.ts | 18 ++- .../simulator/src/client/simulator.test.ts | 33 +++-- .../simulator/src/client/simulator.ts | 26 ++-- 26 files changed, 297 insertions(+), 135 deletions(-) rename noir/noir-repo/aztec_macros/src/transforms/{compute_note_hash_and_nullifier.rs => compute_note_hash_and_optionally_a_nullifier.rs} (80%) diff --git a/boxes/boxes/react/src/config.ts b/boxes/boxes/react/src/config.ts index 65a47ca03c9..12abd35546d 100644 --- a/boxes/boxes/react/src/config.ts +++ b/boxes/boxes/react/src/config.ts @@ -28,5 +28,5 @@ export class PrivateEnv { export const deployerEnv = new PrivateEnv(SECRET_KEY, process.env.PXE_URL || 'http://localhost:8080'); -const IGNORE_FUNCTIONS = ['constructor', 'compute_note_hash_and_nullifier']; +const IGNORE_FUNCTIONS = ['constructor', 'compute_note_hash_and_optionally_a_nullifier']; export const filteredInterface = BoxReactContractArtifact.functions.filter(f => !IGNORE_FUNCTIONS.includes(f.name)); diff --git a/docs/docs/getting_started/aztecnr-getting-started.md b/docs/docs/getting_started/aztecnr-getting-started.md index 544fffbb4e0..000e1c0d1d8 100644 --- a/docs/docs/getting_started/aztecnr-getting-started.md +++ b/docs/docs/getting_started/aztecnr-getting-started.md @@ -128,7 +128,7 @@ The `increment` function works very similarly to the `constructor`, but instead ## Prevent double spending -Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its nullifier key. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called `compute_note_hash_and_nullifier` to determine these values for any given note produced by this contract. +Because our counters are private, the network can't directly verify if a note was spent or not, which could lead to double-spending. To solve this, we use a nullifier - a unique identifier generated from each spent note and its nullifier key. Although this isn't really an issue in this simple smart contract, Aztec injects a special function called `compute_note_hash_and_optionally_a_nullifier` to determine these values for any given note produced by this contract. ## Getting a counter diff --git a/docs/docs/guides/smart_contracts/writing_contracts/how_to_emit_event.md b/docs/docs/guides/smart_contracts/writing_contracts/how_to_emit_event.md index c25476e3c59..fadff492e52 100644 --- a/docs/docs/guides/smart_contracts/writing_contracts/how_to_emit_event.md +++ b/docs/docs/guides/smart_contracts/writing_contracts/how_to_emit_event.md @@ -47,7 +47,7 @@ If the decryption is successful, the PXE will store the decrypted note inside a If the decryption fails, the specific log will be discarded. For the PXE to successfully process the decrypted note we need to compute the note's 'note hash' and 'nullifier'. -Aztec.nr enables smart contract developers to design custom notes, meaning developers can also customize how a note's note hash and nullifier should be computed. Because of this customizability, and because there will be a potentially-unlimited number of smart contracts deployed to Aztec, an PXE needs to be 'taught' how to compute the custom note hashes and nullifiers for a particular contract. This is done by a function called `compute_note_hash_and_nullifier`, which is automatically injected into every contract when compiled. +Aztec.nr enables smart contract developers to design custom notes, meaning developers can also customize how a note's note hash and nullifier should be computed. Because of this customizability, and because there will be a potentially-unlimited number of smart contracts deployed to Aztec, an PXE needs to be 'taught' how to compute the custom note hashes and nullifiers for a particular contract. This is done by a function called `compute_note_hash_and_optionally_a_nullifier`, which is automatically injected into every contract when compiled. ## Encrypted Events diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index d184334bfc6..9e842c857ad 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -99,13 +99,13 @@ library Constants { uint256 internal constant DA_GAS_PER_BYTE = 16; uint256 internal constant FIXED_DA_GAS = 512; uint256 internal constant CANONICAL_KEY_REGISTRY_ADDRESS = - 9735143693259978736521448915549382765209954358646272896519366195578572330622; + 2153455745675440165069577621832684870696142028027528497509357256345838682961; uint256 internal constant DEPLOYER_CONTRACT_ADDRESS = - 1330791240588942273989478952163154931941860232471291360599950658792066893795; + 19511485909966796736993840362353440247573331327062358513665772226446629198132; uint256 internal constant REGISTERER_CONTRACT_ADDRESS = - 12230492553436229472833564540666503591270810173190529382505862577652523721217; + 21791696151759019003097706094037044371210776294983020497737005968946992649239; uint256 internal constant GAS_TOKEN_ADDRESS = - 21054354231481372816168706751151469079551620620213512837742215289221210616379; + 3159976153131520272419617514531889581796079438158800470341967144801191524489; uint256 internal constant AZTEC_ADDRESS_LENGTH = 1; uint256 internal constant GAS_FEES_LENGTH = 2; uint256 internal constant GAS_LENGTH = 2; diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index febcbbaf701..cc98cdf96a5 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -104,11 +104,12 @@ pub fn compute_note_hash_for_consumption(note: Note) -> Field where } } -pub fn compute_note_hash_and_nullifier( - // docs:start:compute_note_hash_and_nullifier_args +pub fn compute_note_hash_and_optionally_a_nullifier( + // docs:start:compute_note_hash_and_optionally_a_nullifier_args deserialize_content: fn([Field; N]) -> T, note_header: NoteHeader, - serialized_note: [Field; S] // docs:end:compute_note_hash_and_nullifier_args + compute_nullifier: bool, + serialized_note: [Field; S] // docs:end:compute_note_hash_and_optionally_a_nullifier_args ) -> [Field; 4] where T: NoteInterface { let mut note = deserialize_content(arr_copy_slice(serialized_note, [0; N], 0)); // TODO: change this to note.set_header(header) once https://github.com/noir-lang/noir/issues/4095 is fixed @@ -126,8 +127,12 @@ pub fn compute_note_hash_and_nullifier( let siloed_note_hash = compute_siloed_hash(note_header.contract_address, unique_note_hash); - let inner_nullifier = note.compute_nullifier_without_context(); - // docs:start:compute_note_hash_and_nullifier_returns + let inner_nullifier = if compute_nullifier { + note.compute_nullifier_without_context() + } else { + 0 + }; + // docs:start:compute_note_hash_and_optionally_a_nullifier_returns [inner_note_hash, unique_note_hash, siloed_note_hash, inner_nullifier] - // docs:end:compute_note_hash_and_nullifier_returns + // docs:end:compute_note_hash_and_optionally_a_nullifier_returns } diff --git a/noir-projects/aztec-nr/aztec/src/prelude.nr b/noir-projects/aztec-nr/aztec/src/prelude.nr index 15f3fdc7577..a280ee03154 100644 --- a/noir-projects/aztec-nr/aztec/src/prelude.nr +++ b/noir-projects/aztec-nr/aztec/src/prelude.nr @@ -13,7 +13,7 @@ use crate::{ note::{ note_header::NoteHeader, note_interface::NoteInterface, note_getter_options::NoteGetterOptions, note_viewer_options::NoteViewerOptions, - utils::compute_note_hash_and_nullifier as utils_compute_note_hash_and_nullifier + utils::compute_note_hash_and_optionally_a_nullifier as utils_compute_note_hash_and_optionally_a_nullifier } }; // docs:end:prelude diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 48a622fff9b..05506e03e0d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -146,10 +146,10 @@ global DA_GAS_PER_BYTE: u32 = 16; global FIXED_DA_GAS: u32 = 512; // CANONICAL CONTRACT ADDRESSES -global CANONICAL_KEY_REGISTRY_ADDRESS = 0x1585e564a60e6ec974bc151b62705292ebfc75c33341986a47fd9749cedb567e; -global DEPLOYER_CONTRACT_ADDRESS = 0x02f1337e8c79dd0247ccbde85241ad65ee991ae283a63479e095e51f0abbc7e3; -global REGISTERER_CONTRACT_ADDRESS = 0x1b0a36a60d2a1a358a328feb38cb38244429d8f57d25510083795d0491e1e201; -global GAS_TOKEN_ADDRESS = 0x2e8c579a24417ffdfe9e28139023ed46748e64508e20d7d63163ef7a0732b23b; +global CANONICAL_KEY_REGISTRY_ADDRESS = 0x04c2d010f88e8c238882fbbcbce5c81fdc1dc8ece85e8dbf3f602b4d81ec0351; +global DEPLOYER_CONTRACT_ADDRESS = 0x2b231c13768709b1ba51c1f86275b47e38dfac16e3d7f242cb578d92a4e2d934; +global REGISTERER_CONTRACT_ADDRESS = 0x302da9b6000a76691341b250565ca5c67723261fa99af1435ffe5178ccb21417; +global GAS_TOKEN_ADDRESS = 0x06fc7badd50bb8ee32439b52e8874b5a16ddd2aa1d5647ec46b2a0f51356f889; // LENGTH OF STRUCTS SERIALIZED TO FIELDS global AZTEC_ADDRESS_LENGTH = 1; diff --git a/noir/noir-repo/aztec_macros/src/lib.rs b/noir/noir-repo/aztec_macros/src/lib.rs index 2daf86dc643..a36b7b17d09 100644 --- a/noir/noir-repo/aztec_macros/src/lib.rs +++ b/noir/noir-repo/aztec_macros/src/lib.rs @@ -3,7 +3,7 @@ mod utils; use noirc_errors::Location; use transforms::{ - compute_note_hash_and_nullifier::inject_compute_note_hash_and_nullifier, + compute_note_hash_and_optionally_a_nullifier::inject_compute_note_hash_and_optionally_a_nullifier, contract_interface::{ generate_contract_interface, stub_function, update_fn_signatures_in_contract_interface, }, @@ -236,7 +236,7 @@ fn transform_hir( ) -> Result<(), (AztecMacroError, FileId)> { if has_aztec_dependency(crate_id, context) { transform_events(crate_id, context)?; - inject_compute_note_hash_and_nullifier(crate_id, context)?; + inject_compute_note_hash_and_optionally_a_nullifier(crate_id, context)?; assign_storage_slots(crate_id, context)?; inject_note_exports(crate_id, context)?; update_fn_signatures_in_contract_interface(crate_id, context) diff --git a/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs b/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_optionally_a_nullifier.rs similarity index 80% rename from noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs rename to noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_optionally_a_nullifier.rs index f624cde9969..30c0f63a2d4 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_nullifier.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/compute_note_hash_and_optionally_a_nullifier.rs @@ -14,16 +14,16 @@ use crate::utils::{ }, }; -// Check if "compute_note_hash_and_nullifier(AztecAddress,Field,Field,Field,[Field; N]) -> [Field; 4]" is defined -fn check_for_compute_note_hash_and_nullifier_definition( +// Check if "compute_note_hash_and_optionally_a_nullifier(AztecAddress,Field,Field,Field,bool,[Field; N]) -> [Field; 4]" is defined +fn check_for_compute_note_hash_and_optionally_a_nullifier_definition( crate_id: &CrateId, context: &HirContext, ) -> bool { collect_crate_functions(crate_id, context).iter().any(|funct_id| { let func_data = context.def_interner.function_meta(funct_id); let func_name = context.def_interner.function_name(funct_id); - func_name == "compute_note_hash_and_nullifier" - && func_data.parameters.len() == 5 + func_name == "compute_note_hash_and_optionally_a_nullifier" + && func_data.parameters.len() == 6 && func_data.parameters.0.first().is_some_and(| (_, typ, _) | match typ { Type::Struct(struct_typ, _) => struct_typ.borrow().name.0.contents == "AztecAddress", _ => false @@ -31,8 +31,9 @@ fn check_for_compute_note_hash_and_nullifier_definition( && func_data.parameters.0.get(1).is_some_and(|(_, typ, _)| typ.is_field()) && func_data.parameters.0.get(2).is_some_and(|(_, typ, _)| typ.is_field()) && func_data.parameters.0.get(3).is_some_and(|(_, typ, _)| typ.is_field()) - // checks if the 5th parameter is an array and contains only fields - && func_data.parameters.0.get(4).is_some_and(|(_, typ, _)| match typ { + && func_data.parameters.0.get(4).is_some_and(|(_, typ, _)| typ.is_bool()) + // checks if the 6th parameter is an array and contains only fields + && func_data.parameters.0.get(5).is_some_and(|(_, typ, _)| match typ { Type::Array(_, inner_type) => inner_type.to_owned().is_field(), _ => false }) @@ -49,16 +50,16 @@ fn check_for_compute_note_hash_and_nullifier_definition( }) } -pub fn inject_compute_note_hash_and_nullifier( +pub fn inject_compute_note_hash_and_optionally_a_nullifier( crate_id: &CrateId, context: &mut HirContext, ) -> Result<(), (AztecMacroError, FileId)> { if let Some((_, module_id, file_id)) = get_contract_module_data(context, crate_id) { - // If compute_note_hash_and_nullifier is already defined by the user, we skip auto-generation in order to provide an + // If compute_note_hash_and_optionally_a_nullifier is already defined by the user, we skip auto-generation in order to provide an // escape hatch for this mechanism. // TODO(#4647): improve this diagnosis and error messaging. if context.crate_graph.root_crate_id() != crate_id - || check_for_compute_note_hash_and_nullifier_definition(crate_id, context) + || check_for_compute_note_hash_and_optionally_a_nullifier_definition(crate_id, context) { return Ok(()); } @@ -69,14 +70,14 @@ pub fn inject_compute_note_hash_and_nullifier( let max_note_length_const = get_global_numberic_const(context, "MAX_NOTE_FIELDS_LENGTH") .map_err(|err| { ( - AztecMacroError::CouldNotImplementComputeNoteHashAndNullifier { + AztecMacroError::CouldNotImplementComputeNoteHashAndOptionallyANullifier { secondary_message: Some(err.primary_message), }, file_id, ) })?; - // In order to implement compute_note_hash_and_nullifier, we need to know all of the different note types the + // In order to implement compute_note_hash_and_optionally_a_nullifier, we need to know all of the different note types the // contract might use and their serialized lengths. These are the types that are marked as #[aztec(note)]. let mut notes_and_lengths = vec![]; @@ -89,7 +90,7 @@ pub fn inject_compute_note_hash_and_nullifier( ) .map_err(|_err| { ( - AztecMacroError::CouldNotImplementComputeNoteHashAndNullifier { + AztecMacroError::CouldNotImplementComputeNoteHashAndOptionallyANullifier { secondary_message: Some(format!( "Failed to get serialized length for note type {}", path @@ -102,7 +103,7 @@ pub fn inject_compute_note_hash_and_nullifier( if serialized_len > max_note_length_const { return Err(( - AztecMacroError::CouldNotImplementComputeNoteHashAndNullifier { + AztecMacroError::CouldNotImplementComputeNoteHashAndOptionallyANullifier { secondary_message: Some(format!( "Note type {} as {} fields, which is more than the maximum allowed length of {}.", path, @@ -120,11 +121,12 @@ pub fn inject_compute_note_hash_and_nullifier( let max_note_length: u128 = *notes_and_lengths.iter().map(|(_, serialized_len)| serialized_len).max().unwrap_or(&0); - let note_types = + let note_types: Vec = notes_and_lengths.iter().map(|(note_type, _)| note_type.clone()).collect::>(); - // We can now generate a version of compute_note_hash_and_nullifier tailored for the contract in this crate. - let func = generate_compute_note_hash_and_nullifier(¬e_types, max_note_length); + // We can now generate a version of compute_note_hash_and_optionally_a_nullifier tailored for the contract in this crate. + let func = + generate_compute_note_hash_and_optionally_a_nullifier(¬e_types, max_note_length); // And inject the newly created function into the contract. @@ -134,7 +136,7 @@ pub fn inject_compute_note_hash_and_nullifier( inject_fn(crate_id, context, func, location, module_id, file_id).map_err(|err| { ( - AztecMacroError::CouldNotImplementComputeNoteHashAndNullifier { + AztecMacroError::CouldNotImplementComputeNoteHashAndOptionallyANullifier { secondary_message: err.secondary_message, }, file_id, @@ -144,12 +146,12 @@ pub fn inject_compute_note_hash_and_nullifier( Ok(()) } -fn generate_compute_note_hash_and_nullifier( +fn generate_compute_note_hash_and_optionally_a_nullifier( note_types: &[String], max_note_length: u128, ) -> NoirFunction { let function_source = - generate_compute_note_hash_and_nullifier_source(note_types, max_note_length); + generate_compute_note_hash_and_optionally_a_nullifier_source(note_types, max_note_length); let (function_ast, errors) = parse_program(&function_source); if !errors.is_empty() { @@ -161,7 +163,7 @@ fn generate_compute_note_hash_and_nullifier( function_ast.functions.remove(0) } -fn generate_compute_note_hash_and_nullifier_source( +fn generate_compute_note_hash_and_optionally_a_nullifier_source( note_types: &[String], max_note_length: u128, ) -> String { @@ -173,12 +175,13 @@ fn generate_compute_note_hash_and_nullifier_source( // so we include a dummy version. format!( " - unconstrained fn compute_note_hash_and_nullifier( + unconstrained fn compute_note_hash_and_optionally_a_nullifier( contract_address: dep::aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field, - serialized_note: [Field; {}] + compute_nullifier: bool, + serialized_note: [Field; {}], ) -> pub [Field; 4] {{ assert(false, \"This contract does not use private notes\"); [0, 0, 0, 0] @@ -191,7 +194,7 @@ fn generate_compute_note_hash_and_nullifier_source( let if_statements: Vec = note_types.iter().map(|note_type| format!( "if (note_type_id == {0}::get_note_type_id()) {{ - dep::aztec::note::utils::compute_note_hash_and_nullifier({0}::deserialize_content, note_header, serialized_note) + dep::aztec::note::utils::compute_note_hash_and_optionally_a_nullifier({0}::deserialize_content, note_header, compute_nullifier, serialized_note) }}" , note_type)).collect(); @@ -204,12 +207,13 @@ fn generate_compute_note_hash_and_nullifier_source( format!( " - unconstrained fn compute_note_hash_and_nullifier( + unconstrained fn compute_note_hash_and_optionally_a_nullifier( contract_address: dep::aztec::protocol_types::address::AztecAddress, nonce: Field, storage_slot: Field, note_type_id: Field, - serialized_note: [Field; {}] + compute_nullifier: bool, + serialized_note: [Field; {}], ) -> pub [Field; 4] {{ let note_header = dep::aztec::prelude::NoteHeader::new(contract_address, nonce, storage_slot); diff --git a/noir/noir-repo/aztec_macros/src/transforms/mod.rs b/noir/noir-repo/aztec_macros/src/transforms/mod.rs index 2a6fef7647f..bd419bced6f 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/mod.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/mod.rs @@ -1,4 +1,4 @@ -pub mod compute_note_hash_and_nullifier; +pub mod compute_note_hash_and_optionally_a_nullifier; pub mod contract_interface; pub mod events; pub mod functions; diff --git a/noir/noir-repo/aztec_macros/src/utils/errors.rs b/noir/noir-repo/aztec_macros/src/utils/errors.rs index 51aea3d052f..852b5f1e57a 100644 --- a/noir/noir-repo/aztec_macros/src/utils/errors.rs +++ b/noir/noir-repo/aztec_macros/src/utils/errors.rs @@ -12,7 +12,7 @@ pub enum AztecMacroError { UnsupportedFunctionReturnType { span: Span, typ: ast::UnresolvedTypeData }, UnsupportedStorageType { span: Option, typ: ast::UnresolvedTypeData }, CouldNotAssignStorageSlots { secondary_message: Option }, - CouldNotImplementComputeNoteHashAndNullifier { secondary_message: Option }, + CouldNotImplementComputeNoteHashAndOptionallyANullifier { secondary_message: Option }, CouldNotImplementNoteInterface { span: Option, secondary_message: Option }, MultipleStorageDefinitions { span: Option }, CouldNotExportStorageLayout { span: Option, secondary_message: Option }, @@ -57,8 +57,8 @@ impl From for MacroError { secondary_message, span: None, }, - AztecMacroError::CouldNotImplementComputeNoteHashAndNullifier { secondary_message } => MacroError { - primary_message: "Could not implement compute_note_hash_and_nullifier automatically, please provide an implementation".to_string(), + AztecMacroError::CouldNotImplementComputeNoteHashAndOptionallyANullifier { secondary_message } => MacroError { + primary_message: "Could not implement compute_note_hash_and_optionally_a_nullifier automatically, please provide an implementation".to_string(), secondary_message, span: None, }, diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs index ff8b11f3916..1357ea09f94 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir_def/types.rs @@ -598,6 +598,10 @@ impl Type { matches!(self.follow_bindings(), Type::FieldElement) } + pub fn is_bool(&self) -> bool { + matches!(self.follow_bindings(), Type::Bool) + } + pub fn is_signed(&self) -> bool { matches!(self.follow_bindings(), Type::Integer(Signedness::Signed, _)) } diff --git a/yarn-project/aztec.js/src/wallet/base_wallet.ts b/yarn-project/aztec.js/src/wallet/base_wallet.ts index a2bd9a6f5b3..604f6ffb774 100644 --- a/yarn-project/aztec.js/src/wallet/base_wallet.ts +++ b/yarn-project/aztec.js/src/wallet/base_wallet.ts @@ -134,6 +134,9 @@ export abstract class BaseWallet implements Wallet { addNote(note: ExtendedNote): Promise { return this.pxe.addNote(note); } + addNullifiedNote(note: ExtendedNote): Promise { + return this.pxe.addNullifiedNote(note); + } getBlock(number: number): Promise { return this.pxe.getBlock(number); } diff --git a/yarn-project/circuit-types/src/interfaces/pxe.ts b/yarn-project/circuit-types/src/interfaces/pxe.ts index 4df12dc8395..2f090abbe0e 100644 --- a/yarn-project/circuit-types/src/interfaces/pxe.ts +++ b/yarn-project/circuit-types/src/interfaces/pxe.ts @@ -239,6 +239,16 @@ export interface PXE { */ addNote(note: ExtendedNote): Promise; + /** + * Adds a nullified note to the database. + * @throws If the note hash of the note doesn't exist in the tree. + * @param note - The note to add. + * @dev We are not deriving a nullifier in this function since that would require having the nullifier secret key + * which is undesirable. Instead, we are just adding the note to the database as nullified and the nullifier is set + * to 0 in the db. + */ + addNullifiedNote(note: ExtendedNote): Promise; + /** * Get the given block. * @param number - The block number being requested. diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index daf3ec870f0..a690d935505 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -85,11 +85,11 @@ export const DA_BYTES_PER_FIELD = 32; export const DA_GAS_PER_BYTE = 16; export const FIXED_DA_GAS = 512; export const CANONICAL_KEY_REGISTRY_ADDRESS = - 9735143693259978736521448915549382765209954358646272896519366195578572330622n; -export const DEPLOYER_CONTRACT_ADDRESS = 1330791240588942273989478952163154931941860232471291360599950658792066893795n; + 2153455745675440165069577621832684870696142028027528497509357256345838682961n; +export const DEPLOYER_CONTRACT_ADDRESS = 19511485909966796736993840362353440247573331327062358513665772226446629198132n; export const REGISTERER_CONTRACT_ADDRESS = - 12230492553436229472833564540666503591270810173190529382505862577652523721217n; -export const GAS_TOKEN_ADDRESS = 21054354231481372816168706751151469079551620620213512837742215289221210616379n; + 21791696151759019003097706094037044371210776294983020497737005968946992649239n; +export const GAS_TOKEN_ADDRESS = 3159976153131520272419617514531889581796079438158800470341967144801191524489n; export const AZTEC_ADDRESS_LENGTH = 1; export const GAS_FEES_LENGTH = 2; export const GAS_LENGTH = 2; diff --git a/yarn-project/cli/src/cmds/inspect_contract.ts b/yarn-project/cli/src/cmds/inspect_contract.ts index 49967561084..09284f3abe8 100644 --- a/yarn-project/cli/src/cmds/inspect_contract.ts +++ b/yarn-project/cli/src/cmds/inspect_contract.ts @@ -12,7 +12,7 @@ import { getContractArtifact } from '../utils.js'; export async function inspectContract(contractArtifactFile: string, debugLogger: DebugLogger, log: LogFn) { const contractArtifact = await getContractArtifact(contractArtifactFile, log); - const contractFns = contractArtifact.functions.filter(f => f.name !== 'compute_note_hash_and_nullifier'); + const contractFns = contractArtifact.functions.filter(f => f.name !== 'compute_note_hash_and_optionally_a_nullifier'); if (contractFns.length === 0) { log(`No functions found for contract ${contractArtifact.name}`); } diff --git a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts index 0d3f77be751..761f69c1613 100644 --- a/yarn-project/end-to-end/src/e2e_2_pxes.test.ts +++ b/yarn-project/end-to-end/src/e2e_2_pxes.test.ts @@ -12,9 +12,9 @@ import { computeSecretHash, retryUntil, } from '@aztec/aztec.js'; -import { ChildContract, TokenContract } from '@aztec/noir-contracts.js'; +import { ChildContract, TestContract, TokenContract } from '@aztec/noir-contracts.js'; -import { jest } from '@jest/globals'; +import { expect, jest } from '@jest/globals'; import { expectsNumOfNoteEncryptedLogsInTheLastBlockToBe, setup, setupPXEService } from './fixtures/utils.js'; @@ -88,7 +88,7 @@ describe('e2e_2_pxes', () => { logger.info('L2 contract deployed'); - return contract.instance; + return contract; }; const mintTokens = async ( @@ -122,8 +122,7 @@ describe('e2e_2_pxes', () => { const transferAmount1 = 654n; const transferAmount2 = 323n; - const tokenInstance = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); - const tokenAddress = tokenInstance.address; + const token = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); // Add account B to wallet A await pxeA.registerRecipient(walletB.getCompleteAddress()); @@ -131,30 +130,27 @@ describe('e2e_2_pxes', () => { await pxeB.registerRecipient(walletA.getCompleteAddress()); // Add token to PXE B (PXE A already has it because it was deployed through it) - await pxeB.registerContract({ - artifact: TokenContract.artifact, - instance: tokenInstance, - }); + await pxeB.registerContract(token); // Check initial balances and logs are as expected - await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance); - await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), 0n); + await expectTokenBalance(walletA, token.address, walletA.getAddress(), initialBalance); + await expectTokenBalance(walletB, token.address, walletB.getAddress(), 0n); await expectsNumOfNoteEncryptedLogsInTheLastBlockToBe(aztecNode, 1); // Transfer funds from A to B via PXE A - const contractWithWalletA = await TokenContract.at(tokenAddress, walletA); + const contractWithWalletA = await TokenContract.at(token.address, walletA); await contractWithWalletA.methods .transfer(walletA.getAddress(), walletB.getAddress(), transferAmount1, 0) .send() .wait(); // Check balances and logs are as expected - await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance - transferAmount1); - await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), transferAmount1); + await expectTokenBalance(walletA, token.address, walletA.getAddress(), initialBalance - transferAmount1); + await expectTokenBalance(walletB, token.address, walletB.getAddress(), transferAmount1); await expectsNumOfNoteEncryptedLogsInTheLastBlockToBe(aztecNode, 2); // Transfer funds from B to A via PXE B - const contractWithWalletB = await TokenContract.at(tokenAddress, walletB); + const contractWithWalletB = await TokenContract.at(token.address, walletB); await contractWithWalletB.methods .transfer(walletB.getAddress(), walletA.getAddress(), transferAmount2, 0) .send() @@ -163,11 +159,11 @@ describe('e2e_2_pxes', () => { // Check balances and logs are as expected await expectTokenBalance( walletA, - tokenAddress, + token.address, walletA.getAddress(), initialBalance - transferAmount1 + transferAmount2, ); - await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), transferAmount1 - transferAmount2); + await expectTokenBalance(walletB, token.address, walletB.getAddress(), transferAmount1 - transferAmount2); await expectsNumOfNoteEncryptedLogsInTheLastBlockToBe(aztecNode, 2); }); @@ -218,8 +214,8 @@ describe('e2e_2_pxes', () => { const userABalance = 100n; const userBBalance = 150n; - const tokenInstance = await deployTokenContract(userABalance, walletA.getAddress(), pxeA); - const contractWithWalletA = await TokenContract.at(tokenInstance.address, walletA); + const token = await deployTokenContract(userABalance, walletA.getAddress(), pxeA); + const contractWithWalletA = await TokenContract.at(token.address, walletA); // Add account B to wallet A await pxeA.registerRecipient(walletB.getCompleteAddress()); @@ -227,27 +223,24 @@ describe('e2e_2_pxes', () => { await pxeB.registerRecipient(walletA.getCompleteAddress()); // Add token to PXE B (PXE A already has it because it was deployed through it) - await pxeB.registerContract({ - artifact: TokenContract.artifact, - instance: tokenInstance, - }); + await pxeB.registerContract(token); // Mint tokens to user B - const contractWithWalletB = await TokenContract.at(tokenInstance.address, walletB); + const contractWithWalletB = await TokenContract.at(token.address, walletB); await mintTokens(contractWithWalletA, contractWithWalletB, walletB.getAddress(), userBBalance, pxeB); // Check that user A balance is 100 on server A - await expectTokenBalance(walletA, tokenInstance.address, walletA.getAddress(), userABalance); + await expectTokenBalance(walletA, token.address, walletA.getAddress(), userABalance); // Check that user B balance is 150 on server B - await expectTokenBalance(walletB, tokenInstance.address, walletB.getAddress(), userBBalance); + await expectTokenBalance(walletB, token.address, walletB.getAddress(), userBBalance); // CHECK THAT PRIVATE BALANCES ARE 0 WHEN ACCOUNT'S SECRET KEYS ARE NOT REGISTERED // Note: Not checking if the account is synchronized because it is not registered as an account (it would throw). const checkIfSynchronized = false; // Check that user A balance is 0 on server B - await expectTokenBalance(walletB, tokenInstance.address, walletA.getAddress(), 0n, checkIfSynchronized); + await expectTokenBalance(walletB, token.address, walletA.getAddress(), 0n, checkIfSynchronized); // Check that user B balance is 0 on server A - await expectTokenBalance(walletA, tokenInstance.address, walletB.getAddress(), 0n, checkIfSynchronized); + await expectTokenBalance(walletA, token.address, walletB.getAddress(), 0n, checkIfSynchronized); }); it('permits migrating an account from one PXE to another', async () => { @@ -272,8 +265,8 @@ describe('e2e_2_pxes', () => { const initialBalance = 987n; const transferAmount1 = 654n; - const tokenInstance = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); - const tokenAddress = tokenInstance.address; + const token = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); + const tokenAddress = token.address; // Add account B to wallet A await pxeA.registerRecipient(walletB.getCompleteAddress()); @@ -294,10 +287,7 @@ describe('e2e_2_pxes', () => { .wait(); // now add the contract and check balances - await pxeB.registerContract({ - artifact: TokenContract.artifact, - instance: tokenInstance, - }); + await pxeB.registerContract(token); await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance - transferAmount1); await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), transferAmount1); }); @@ -322,28 +312,27 @@ describe('e2e_2_pxes', () => { await pxeA.registerRecipient(walletB.getCompleteAddress()); // deploy the contract on PXE A - const tokenInstance = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); - const tokenAddress = tokenInstance.address; + const token = await deployTokenContract(initialBalance, walletA.getAddress(), pxeA); // Transfer funds from A to Shared Wallet via PXE A - const contractWithWalletA = await TokenContract.at(tokenAddress, walletA); + const contractWithWalletA = await TokenContract.at(token.address, walletA); await contractWithWalletA.methods .transfer(walletA.getAddress(), sharedAccountAddress.address, transferAmount1, 0) .send() .wait(); // Now send funds from Shared Wallet to B via PXE A - const contractWithSharedWalletA = await TokenContract.at(tokenAddress, sharedWalletOnA); + const contractWithSharedWalletA = await TokenContract.at(token.address, sharedWalletOnA); await contractWithSharedWalletA.methods .transfer(sharedAccountAddress.address, walletB.getAddress(), transferAmount2, 0) .send() .wait(); // check balances from PXE-A's perspective - await expectTokenBalance(walletA, tokenAddress, walletA.getAddress(), initialBalance - transferAmount1); + await expectTokenBalance(walletA, token.address, walletA.getAddress(), initialBalance - transferAmount1); await expectTokenBalance( sharedWalletOnA, - tokenAddress, + token.address, sharedAccountAddress.address, transferAmount1 - transferAmount2, ); @@ -353,17 +342,57 @@ describe('e2e_2_pxes', () => { // PXE-B had previously deferred the notes from A -> Shared, and Shared -> B // PXE-B adds the contract // PXE-B reprocesses the deferred notes, and sees the nullifier for A -> Shared - await pxeB.registerContract({ - artifact: TokenContract.artifact, - instance: tokenInstance, - }); - await expectTokenBalance(walletB, tokenAddress, walletB.getAddress(), transferAmount2); + await pxeB.registerContract(token); + await expectTokenBalance(walletB, token.address, walletB.getAddress(), transferAmount2); await expect(sharedWalletOnB.isAccountStateSynchronized(sharedAccountAddress.address)).resolves.toBe(true); await expectTokenBalance( sharedWalletOnB, - tokenAddress, + token.address, sharedAccountAddress.address, transferAmount1 - transferAmount2, ); }); + + it('adds and fetches a nullified note', async () => { + // 1. Deploys test contract through PXE A + const testContract = await TestContract.deploy(walletA).send().deployed(); + + // 2. Create a note + const noteStorageSlot = 10; + const noteValue = 5; + let note: ExtendedNote; + { + const receipt = await testContract.methods + .call_create_note(noteValue, walletA.getAddress(), walletA.getAddress(), noteStorageSlot) + .send() + .wait({ debug: true }); + const notes = receipt.debugInfo?.visibleNotes; + expect(notes).toHaveLength(1); + note = notes![0]; + } + + // 3. Nullify the note + { + const receipt = await testContract.methods.call_destroy_note(noteStorageSlot).send().wait({ debug: true }); + // Check that we got 2 nullifiers - 1 for tx hash, 1 for the note + expect(receipt.debugInfo?.nullifiers).toHaveLength(2); + } + + // 4. Adds the nullified public key note to PXE B + { + // We need to register the recipient to be able to obtain IvpkM for the note + await pxeB.registerRecipient(walletA.getCompleteAddress()); + // We need to register the contract to be able to compute the note hash by calling compute_note_hash_and_optionally_a_nullifier(...) + await pxeB.registerContract(testContract); + await pxeB.addNullifiedNote(note); + } + + // 5. Try fetching the nullified note + { + const testContractWithWalletB = await TestContract.at(testContract.address, walletB); + const noteValue = await testContractWithWalletB.methods.call_get_notes(noteStorageSlot, true).simulate(); + expect(noteValue).toBe(noteValue); + // --> We have successfully obtained the nullified note from PXE B verifying that pxe.addNullifiedNote(...) works + } + }); }); diff --git a/yarn-project/pxe/src/database/incoming_note_dao.ts b/yarn-project/pxe/src/database/incoming_note_dao.ts index 85684c7cc6a..6db39e1b455 100644 --- a/yarn-project/pxe/src/database/incoming_note_dao.ts +++ b/yarn-project/pxe/src/database/incoming_note_dao.ts @@ -26,7 +26,10 @@ export class IncomingNoteDao implements NoteData { * We can use this value to compute siloedNoteHash and uniqueSiloedNoteHash. */ public innerNoteHash: Fr, - /** The nullifier of the note (siloed by contract address). */ + /** + * The nullifier of the note (siloed by contract address). + * Note: Might be set as 0 if the note was added to PXE as nullified. + */ public siloedNullifier: Fr, /** The location of the relevant note in the note hash tree. */ public index: bigint, diff --git a/yarn-project/pxe/src/database/kv_pxe_database.ts b/yarn-project/pxe/src/database/kv_pxe_database.ts index 3288433f52a..74f41fbae8d 100644 --- a/yarn-project/pxe/src/database/kv_pxe_database.ts +++ b/yarn-project/pxe/src/database/kv_pxe_database.ts @@ -334,6 +334,18 @@ export class KVPxeDatabase implements PxeDatabase { }); } + async addNullifiedNote(note: IncomingNoteDao): Promise { + const noteIndex = toBufferBE(note.index, 32).toString('hex'); + + await this.#nullifiedNotes.set(noteIndex, note.toBuffer()); + await this.#nullifiedNotesByContract.set(note.contractAddress.toString(), noteIndex); + await this.#nullifiedNotesByStorageSlot.set(note.storageSlot.toString(), noteIndex); + await this.#nullifiedNotesByTxHash.set(note.txHash.toString(), noteIndex); + await this.#nullifiedNotesByIvpkM.set(note.ivpkM.toString(), noteIndex); + + return Promise.resolve(); + } + async setHeader(header: Header): Promise { await this.#synchronizedBlock.set(header.toBuffer()); } diff --git a/yarn-project/pxe/src/database/pxe_database.ts b/yarn-project/pxe/src/database/pxe_database.ts index f40a11334e9..a5ffb5fadf1 100644 --- a/yarn-project/pxe/src/database/pxe_database.ts +++ b/yarn-project/pxe/src/database/pxe_database.ts @@ -59,6 +59,12 @@ export interface PxeDatabase extends ContractArtifactDatabase, ContractInstanceD */ addNote(note: IncomingNoteDao): Promise; + /** + * Adds a nullified note to DB. + * @param note - The note to add. + */ + addNullifiedNote(note: IncomingNoteDao): Promise; + /** * Adds an array of notes to DB. * This function is used to insert multiple notes to the database at once, diff --git a/yarn-project/pxe/src/note_processor/note_processor.test.ts b/yarn-project/pxe/src/note_processor/note_processor.test.ts index 59f349b4d0b..24de16e5143 100644 --- a/yarn-project/pxe/src/note_processor/note_processor.test.ts +++ b/yarn-project/pxe/src/note_processor/note_processor.test.ts @@ -167,11 +167,11 @@ describe('Note Processor', () => { simulator, ); - simulator.computeNoteHashAndNullifier.mockImplementation((...args) => + simulator.computeNoteHashAndOptionallyANullifier.mockImplementation((...args) => Promise.resolve({ innerNoteHash: Fr.random(), uniqueNoteHash: Fr.random(), - siloedNoteHash: pedersenHash(args[4].items), // args[4] is note + siloedNoteHash: pedersenHash(args[5].items), // args[5] is note innerNullifier: Fr.random(), }), ); diff --git a/yarn-project/pxe/src/note_processor/produce_note_dao.ts b/yarn-project/pxe/src/note_processor/produce_note_dao.ts index 4eaedb79b08..34c90f22962 100644 --- a/yarn-project/pxe/src/note_processor/produce_note_dao.ts +++ b/yarn-project/pxe/src/note_processor/produce_note_dao.ts @@ -152,11 +152,12 @@ async function findNoteIndexAndNullifier( } const expectedNonce = computeNoteHashNonce(firstNullifier, noteHashIndex); - ({ innerNoteHash, siloedNoteHash, innerNullifier } = await simulator.computeNoteHashAndNullifier( + ({ innerNoteHash, siloedNoteHash, innerNullifier } = await simulator.computeNoteHashAndOptionallyANullifier( contractAddress, expectedNonce, storageSlot, noteTypeId, + true, note, )); diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index cf329063fbd..63628b8859f 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -318,13 +318,15 @@ export class PXEService implements PXE { } for (const nonce of nonces) { - const { innerNoteHash, siloedNoteHash, innerNullifier } = await this.simulator.computeNoteHashAndNullifier( - note.contractAddress, - nonce, - note.storageSlot, - note.noteTypeId, - note.note, - ); + const { innerNoteHash, siloedNoteHash, innerNullifier } = + await this.simulator.computeNoteHashAndOptionallyANullifier( + note.contractAddress, + nonce, + note.storageSlot, + note.noteTypeId, + true, + note.note, + ); const index = await this.node.findLeafIndex('latest', MerkleTreeId.NOTE_HASH_TREE, siloedNoteHash); if (index === undefined) { @@ -354,6 +356,54 @@ export class PXEService implements PXE { } } + public async addNullifiedNote(note: ExtendedNote) { + const owner = await this.db.getCompleteAddress(note.owner); + if (!owner) { + throw new Error(`Unknown account: ${note.owner.toString()}`); + } + + const nonces = await this.getNoteNonces(note); + if (nonces.length === 0) { + throw new Error(`Cannot find the note in tx: ${note.txHash}.`); + } + + for (const nonce of nonces) { + const { innerNoteHash, siloedNoteHash, innerNullifier } = + await this.simulator.computeNoteHashAndOptionallyANullifier( + note.contractAddress, + nonce, + note.storageSlot, + note.noteTypeId, + false, + note.note, + ); + + if (!innerNullifier.equals(Fr.ZERO)) { + throw new Error('Unexpectedly received non-zero nullifier.'); + } + + const index = await this.node.findLeafIndex('latest', MerkleTreeId.NOTE_HASH_TREE, siloedNoteHash); + if (index === undefined) { + throw new Error('Note does not exist.'); + } + + await this.db.addNullifiedNote( + new IncomingNoteDao( + note.note, + note.contractAddress, + note.storageSlot, + note.noteTypeId, + note.txHash, + nonce, + innerNoteHash, + Fr.ZERO, // We are not able to derive + index, + owner.publicKeys.masterIncomingViewingPublicKey, + ), + ); + } + } + /** * Finds the nonce(s) for a given note. * @param note - The note to find the nonces for. @@ -373,11 +423,12 @@ export class PXEService implements PXE { // Remove this once notes added from public also include nonces. { const publicNoteNonce = Fr.ZERO; - const { siloedNoteHash } = await this.simulator.computeNoteHashAndNullifier( + const { siloedNoteHash } = await this.simulator.computeNoteHashAndOptionallyANullifier( note.contractAddress, publicNoteNonce, note.storageSlot, note.noteTypeId, + false, note.note, ); if (tx.noteHashes.some(hash => hash.equals(siloedNoteHash))) { @@ -394,11 +445,12 @@ export class PXEService implements PXE { } const nonce = computeNoteHashNonce(firstNullifier, i); - const { siloedNoteHash } = await this.simulator.computeNoteHashAndNullifier( + const { siloedNoteHash } = await this.simulator.computeNoteHashAndOptionallyANullifier( note.contractAddress, nonce, note.storageSlot, note.noteTypeId, + false, note.note, ); if (hash.equals(siloedNoteHash)) { diff --git a/yarn-project/simulator/src/client/private_execution.test.ts b/yarn-project/simulator/src/client/private_execution.test.ts index 6b18d222713..5946f7c0528 100644 --- a/yarn-project/simulator/src/client/private_execution.test.ts +++ b/yarn-project/simulator/src/client/private_execution.test.ts @@ -441,7 +441,14 @@ describe('Private Execution test suite', () => { oracle.getNotes.mockResolvedValue(notes); const consumedNotes = await asyncMap(notes, ({ nonce, note }) => - acirSimulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, valueNoteTypeId, note), + acirSimulator.computeNoteHashAndOptionallyANullifier( + contractAddress, + nonce, + storageSlot, + valueNoteTypeId, + true, + note, + ), ); await insertLeaves(consumedNotes.map(n => n.siloedNoteHash)); @@ -511,7 +518,14 @@ describe('Private Execution test suite', () => { oracle.getNotes.mockResolvedValue(notes); const consumedNotes = await asyncMap(notes, ({ nonce, note }) => - acirSimulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, valueNoteTypeId, note), + acirSimulator.computeNoteHashAndOptionallyANullifier( + contractAddress, + nonce, + storageSlot, + valueNoteTypeId, + true, + note, + ), ); await insertLeaves(consumedNotes.map(n => n.siloedNoteHash)); diff --git a/yarn-project/simulator/src/client/simulator.test.ts b/yarn-project/simulator/src/client/simulator.test.ts index 63c468804b2..824cd1def7a 100644 --- a/yarn-project/simulator/src/client/simulator.test.ts +++ b/yarn-project/simulator/src/client/simulator.test.ts @@ -50,8 +50,8 @@ describe('Simulator', () => { simulator = new AcirSimulator(oracle, node); }); - describe('computeNoteHashAndNullifier', () => { - const artifact = getFunctionArtifact(TokenContractArtifact, 'compute_note_hash_and_nullifier'); + describe('computeNoteHashAndOptionallyANullifier', () => { + const artifact = getFunctionArtifact(TokenContractArtifact, 'compute_note_hash_and_optionally_a_nullifier'); const nonce = Fr.random(); const storageSlot = TokenContractArtifact.storageLayout['balances'].slot; const noteTypeId = TokenContractArtifact.notes['TokenNote'].id; @@ -68,7 +68,14 @@ describe('Simulator', () => { const siloedNoteHash = siloNoteHash(contractAddress, uniqueNoteHash); const innerNullifier = poseidon2Hash([siloedNoteHash, appNullifierSecretKey, GeneratorIndex.NOTE_NULLIFIER]); - const result = await simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note); + const result = await simulator.computeNoteHashAndOptionallyANullifier( + contractAddress, + nonce, + storageSlot, + noteTypeId, + true, + note, + ); expect(result).toEqual({ innerNoteHash, @@ -78,16 +85,16 @@ describe('Simulator', () => { }); }); - it('throw if the contract does not implement "compute_note_hash_and_nullifier"', async () => { + it('throw if the contract does not implement "compute_note_hash_and_optionally_a_nullifier"', async () => { oracle.getFunctionArtifactByName.mockResolvedValue(undefined); const note = createNote(); await expect( - simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note), - ).rejects.toThrow(/Mandatory implementation of "compute_note_hash_and_nullifier" missing/); + simulator.computeNoteHashAndOptionallyANullifier(contractAddress, nonce, storageSlot, noteTypeId, true, note), + ).rejects.toThrow(/Mandatory implementation of "compute_note_hash_and_optionally_a_nullifier" missing/); }); - it('throw if "compute_note_hash_and_nullifier" has the wrong number of parameters', async () => { + it('throw if "compute_note_hash_and_optionally_a_nullifier" has the wrong number of parameters', async () => { const note = createNote(); const modifiedArtifact: FunctionArtifact = { @@ -97,15 +104,15 @@ describe('Simulator', () => { oracle.getFunctionArtifactByName.mockResolvedValue(modifiedArtifact); await expect( - simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note), + simulator.computeNoteHashAndOptionallyANullifier(contractAddress, nonce, storageSlot, noteTypeId, true, note), ).rejects.toThrow( new RegExp( - `Expected 5 parameters in mandatory implementation of "compute_note_hash_and_nullifier", but found 4 in noir contract ${contractAddress}.`, + `Expected 6 parameters in mandatory implementation of "compute_note_hash_and_optionally_a_nullifier", but found 5 in noir contract ${contractAddress}.`, ), ); }); - it('throw if a note has more fields than "compute_note_hash_and_nullifier" can process', async () => { + it('throw if a note has more fields than "compute_note_hash_and_optionally_a_nullifier" can process', async () => { const note = createNote(); const wrongPreimageLength = note.length - 1; @@ -129,9 +136,11 @@ describe('Simulator', () => { oracle.getFunctionArtifactByName.mockResolvedValue(modifiedArtifact); await expect( - simulator.computeNoteHashAndNullifier(contractAddress, nonce, storageSlot, noteTypeId, note), + simulator.computeNoteHashAndOptionallyANullifier(contractAddress, nonce, storageSlot, noteTypeId, true, note), ).rejects.toThrow( - new RegExp(`"compute_note_hash_and_nullifier" can only handle a maximum of ${wrongPreimageLength} fields`), + new RegExp( + `"compute_note_hash_and_optionally_a_nullifier" can only handle a maximum of ${wrongPreimageLength} fields`, + ), ); }); }); diff --git a/yarn-project/simulator/src/client/simulator.ts b/yarn-project/simulator/src/client/simulator.ts index 756720bc8aa..0be77660a6a 100644 --- a/yarn-project/simulator/src/client/simulator.ts +++ b/yarn-project/simulator/src/client/simulator.ts @@ -132,29 +132,31 @@ export class AcirSimulator { * @param nonce - The nonce of the note hash. * @param storageSlot - The storage slot. * @param noteTypeId - The note type identifier. + * @param computeNullifier - A flag indicating whether to compute the nullifier or just return 0. * @param note - The note. * @returns The nullifier. */ - public async computeNoteHashAndNullifier( + public async computeNoteHashAndOptionallyANullifier( contractAddress: AztecAddress, nonce: Fr, storageSlot: Fr, noteTypeId: Fr, + computeNullifier: boolean, note: Note, ) { const artifact: FunctionArtifact | undefined = await this.db.getFunctionArtifactByName( contractAddress, - 'compute_note_hash_and_nullifier', + 'compute_note_hash_and_optionally_a_nullifier', ); if (!artifact) { throw new Error( - `Mandatory implementation of "compute_note_hash_and_nullifier" missing in noir contract ${contractAddress.toString()}.`, + `Mandatory implementation of "compute_note_hash_and_optionally_a_nullifier" missing in noir contract ${contractAddress.toString()}.`, ); } - if (artifact.parameters.length != 5) { + if (artifact.parameters.length != 6) { throw new Error( - `Expected 5 parameters in mandatory implementation of "compute_note_hash_and_nullifier", but found ${ + `Expected 6 parameters in mandatory implementation of "compute_note_hash_and_optionally_a_nullifier", but found ${ artifact.parameters.length } in noir contract ${contractAddress.toString()}.`, ); @@ -163,7 +165,7 @@ export class AcirSimulator { const maxNoteFields = (artifact.parameters[artifact.parameters.length - 1].type as ArrayType).length; if (maxNoteFields < note.items.length) { throw new Error( - `The note being processed has ${note.items.length} fields, while "compute_note_hash_and_nullifier" can only handle a maximum of ${maxNoteFields} fields. Please reduce the number of fields in your note.`, + `The note being processed has ${note.items.length} fields, while "compute_note_hash_and_optionally_a_nullifier" can only handle a maximum of ${maxNoteFields} fields. Please reduce the number of fields in your note.`, ); } @@ -175,7 +177,14 @@ export class AcirSimulator { selector: FunctionSelector.empty(), type: FunctionType.UNCONSTRAINED, isStatic: artifact.isStatic, - args: encodeArguments(artifact, [contractAddress, nonce, storageSlot, noteTypeId, extendedNoteItems]), + args: encodeArguments(artifact, [ + contractAddress, + nonce, + storageSlot, + noteTypeId, + computeNullifier, + extendedNoteItems, + ]), returnTypes: artifact.returnTypes, }; @@ -202,11 +211,12 @@ export class AcirSimulator { * @returns The note hash. */ public async computeInnerNoteHash(contractAddress: AztecAddress, storageSlot: Fr, noteTypeId: Fr, note: Note) { - const { innerNoteHash } = await this.computeNoteHashAndNullifier( + const { innerNoteHash } = await this.computeNoteHashAndOptionallyANullifier( contractAddress, Fr.ZERO, storageSlot, noteTypeId, + false, note, ); return innerNoteHash;