diff --git a/docs/docs/developers/contracts/references/storage/main.md b/docs/docs/developers/contracts/references/storage/main.md index 8645931656e..aef6fe7cbf3 100644 --- a/docs/docs/developers/contracts/references/storage/main.md +++ b/docs/docs/developers/contracts/references/storage/main.md @@ -7,7 +7,7 @@ Smart contracts rely on storage, acting as the persistent memory on the blockcha To learn how to define a storage struct, read [this guide](../../writing_contracts/storage/define_storage.md). To learn more about storage slots, read [this explainer](../../writing_contracts/storage/storage_slots.md). -You control this storage in Aztec using the `Storage` struct. This struct serves as the housing unit for all your smart contract's state variables - the data it needs to keep track of and maintain. +You control this storage in Aztec using a struct annotated with `#[aztec(storage)]`. This struct serves as the housing unit for all your smart contract's state variables - the data it needs to keep track of and maintain. These state variables come in two forms: public and private. Public variables are visible to anyone, and private variables remain hidden within the contract. diff --git a/docs/docs/developers/contracts/writing_contracts/storage/define_storage.md b/docs/docs/developers/contracts/writing_contracts/storage/define_storage.md index 6c913bd6b3a..8b47954260a 100644 --- a/docs/docs/developers/contracts/writing_contracts/storage/define_storage.md +++ b/docs/docs/developers/contracts/writing_contracts/storage/define_storage.md @@ -16,4 +16,4 @@ struct Storage { } ``` -If you have defined a `Storage` struct following this naming scheme, then it will be made available to you through the reserved `storage` keyword within your contract functions. +If you have defined a struct and annotated it as `#[aztec(storage)]`, then it will be made available to you through the reserved `storage` keyword within your contract functions. diff --git a/docs/docs/misc/migration_notes.md b/docs/docs/misc/migration_notes.md index be8a430cc31..cccd604642c 100644 --- a/docs/docs/misc/migration_notes.md +++ b/docs/docs/misc/migration_notes.md @@ -8,6 +8,49 @@ Aztec is in full-speed development. Literally every version breaks compatibility ## TBD +### [Aztec.nr] Storage struct annotation + +The storage struct now identified by the annotation `#[aztec(storage)]`, instead of having to rely on it being called `Storage`. + +```diff +- struct Storage { +- ... +- } ++ #[aztec(storage)] ++ struct MyStorageStruct { ++ ... ++ } +``` + +### [Aztec.js] Storage layout and note info + +Storage layout and note information are now exposed in the TS contract artifact + +```diff +- const note = new Note([new Fr(mintAmount), secretHash]); +- const pendingShieldStorageSlot = new Fr(5n); // storage slot for pending_shields +- const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // note type id for TransparentNote +- const extendedNote = new ExtendedNote( +- note, +- admin.address, +- token.address, +- pendingShieldStorageSlot, +- noteTypeId, +- receipt.txHash, +- ); +- await pxe.addNote(extendedNote); ++ const note = new Note([new Fr(mintAmount), secretHash]); ++ const extendedNote = new ExtendedNote( ++ note, ++ admin.address, ++ token.address, ++ TokenContract.storage.pending_shields.slot, ++ TokenContract.notes.TransparentNote.id, ++ receipt.txHash, ++ ); ++ await pxe.addNote(extendedNote); +``` + ### [Aztec.nr] rand oracle is now called unsafe_rand `oracle::rand::rand` has been renamed to `oracle::unsafe_rand::unsafe_rand`. This change was made to communicate that we do not constrain the value in circuit and instead we just trust our PXE. diff --git a/noir-projects/aztec-nr/aztec/src/prelude.nr b/noir-projects/aztec-nr/aztec/src/prelude.nr index 4ff61133372..b43c10713cd 100644 --- a/noir-projects/aztec-nr/aztec/src/prelude.nr +++ b/noir-projects/aztec-nr/aztec/src/prelude.nr @@ -4,7 +4,7 @@ use crate::{ state_vars::{ map::Map, private_immutable::PrivateImmutable, private_mutable::PrivateMutable, public_immutable::PublicImmutable, public_mutable::PublicMutable, private_set::PrivateSet, - shared_immutable::SharedImmutable + shared_immutable::SharedImmutable, storage::Storable }, log::{emit_unencrypted_log, emit_encrypted_log}, context::PrivateContext, note::{ diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr b/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr index e742ab7e036..fa9b21ca11e 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr @@ -5,3 +5,12 @@ trait Storage where T: Serialize + Deserialize { self.storage_slot } } + +// Struct representing an exportable storage variable in the contract +// Every entry in the storage struct will be exported in the compilation artifact as a +// Storable entity, containing the storage slot and the type of the variable +struct Storable { + slot: Field, + typ: str + } + diff --git a/noir/noir-repo/aztec_macros/src/lib.rs b/noir/noir-repo/aztec_macros/src/lib.rs index 56c1377d9a9..1aafda1d093 100644 --- a/noir/noir-repo/aztec_macros/src/lib.rs +++ b/noir/noir-repo/aztec_macros/src/lib.rs @@ -5,16 +5,15 @@ use transforms::{ compute_note_hash_and_nullifier::inject_compute_note_hash_and_nullifier, events::{generate_selector_impl, transform_events}, functions::{transform_function, transform_unconstrained}, - note_interface::generate_note_interface_impl, + note_interface::{generate_note_interface_impl, inject_note_exports}, storage::{ assign_storage_slots, check_for_storage_definition, check_for_storage_implementation, - generate_storage_implementation, + generate_storage_implementation, generate_storage_layout, }, }; -use noirc_frontend::{ - hir::def_collector::dc_crate::{UnresolvedFunctions, UnresolvedTraitImpl}, - macros_api::{CrateId, FileId, HirContext, MacroError, MacroProcessor, SortedModule, Span}, +use noirc_frontend::macros_api::{ + CrateId, FileId, HirContext, MacroError, MacroProcessor, SortedModule, Span, }; use utils::{ @@ -36,16 +35,6 @@ impl MacroProcessor for AztecMacro { transform(ast, crate_id, file_id, context) } - fn process_collected_defs( - &self, - crate_id: &CrateId, - context: &mut HirContext, - collected_trait_impls: &[UnresolvedTraitImpl], - collected_functions: &mut [UnresolvedFunctions], - ) -> Result<(), (MacroError, FileId)> { - transform_collected_defs(crate_id, context, collected_trait_impls, collected_functions) - } - fn process_typed_ast( &self, crate_id: &CrateId, @@ -95,6 +84,7 @@ fn transform_module(module: &mut SortedModule) -> Result if !check_for_storage_implementation(module, &storage_struct_name) { generate_storage_implementation(module, &storage_struct_name)?; } + generate_storage_layout(module, storage_struct_name)?; } for structure in module.types.iter_mut() { @@ -185,24 +175,6 @@ fn transform_module(module: &mut SortedModule) -> Result Ok(has_transformed_module) } -fn transform_collected_defs( - crate_id: &CrateId, - context: &mut HirContext, - collected_trait_impls: &[UnresolvedTraitImpl], - collected_functions: &mut [UnresolvedFunctions], -) -> Result<(), (MacroError, FileId)> { - if has_aztec_dependency(crate_id, context) { - inject_compute_note_hash_and_nullifier( - crate_id, - context, - collected_trait_impls, - collected_functions, - ) - } else { - Ok(()) - } -} - // // Transform Hir Nodes for Aztec // @@ -212,6 +184,12 @@ fn transform_hir( crate_id: &CrateId, context: &mut HirContext, ) -> Result<(), (AztecMacroError, FileId)> { - transform_events(crate_id, context)?; - assign_storage_slots(crate_id, context) + if has_aztec_dependency(crate_id, context) { + transform_events(crate_id, context)?; + inject_compute_note_hash_and_nullifier(crate_id, context)?; + assign_storage_slots(crate_id, context)?; + inject_note_exports(crate_id, context) + } else { + Ok(()) + } } 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_nullifier.rs index 1f5681ed470..1b6630935d9 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_nullifier.rs @@ -1,48 +1,43 @@ use noirc_errors::{Location, Span}; use noirc_frontend::{ graph::CrateId, - hir::{ - def_collector::dc_crate::{UnresolvedFunctions, UnresolvedTraitImpl}, - def_map::{LocalModuleId, ModuleId}, - }, - macros_api::{FileId, HirContext, MacroError}, - node_interner::FuncId, - parse_program, FunctionReturnType, ItemVisibility, NoirFunction, UnresolvedTypeData, + macros_api::{FileId, HirContext}, + parse_program, FunctionReturnType, NoirFunction, Type, UnresolvedTypeData, }; -use crate::utils::hir_utils::fetch_struct_trait_impls; +use crate::utils::{ + errors::AztecMacroError, + hir_utils::{collect_crate_functions, fetch_notes, get_contract_module_data, inject_fn}, +}; // 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( - functions_data: &[(LocalModuleId, FuncId, NoirFunction)], - module_id: LocalModuleId, + crate_id: &CrateId, + context: &HirContext, ) -> bool { - functions_data.iter().filter(|func_data| func_data.0 == module_id).any(|func_data| { - func_data.2.def.name.0.contents == "compute_note_hash_and_nullifier" - && func_data.2.def.parameters.len() == 5 - && match &func_data.2.def.parameters[0].typ.typ { - UnresolvedTypeData::Named(path, _, _) => path.segments.last().unwrap().0.contents == "AztecAddress", - _ => false, - } - && func_data.2.def.parameters[1].typ.typ == UnresolvedTypeData::FieldElement - && func_data.2.def.parameters[2].typ.typ == UnresolvedTypeData::FieldElement - && func_data.2.def.parameters[3].typ.typ == UnresolvedTypeData::FieldElement - // checks if the 5th parameter is an array and the Box in - // Array(Option, Box) contains only fields - && match &func_data.2.def.parameters[4].typ.typ { - UnresolvedTypeData::Array(_, inner_type) => { - matches!(inner_type.typ, UnresolvedTypeData::FieldElement) - }, - _ => false, - } + 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_data.parameters.0.first().is_some_and(| (_, typ, _) | match typ { + Type::Struct(struct_typ, _) => struct_typ.borrow().name.0.contents == "AztecAddress", + _ => false + }) + && 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 { + Type::Array(_, inner_type) => inner_type.to_owned().is_field(), + _ => false + }) // We check the return type the same way as we did the 5th parameter - && match &func_data.2.def.return_type { + && match &func_data.return_type { FunctionReturnType::Default(_) => false, FunctionReturnType::Ty(unresolved_type) => { match &unresolved_type.typ { - UnresolvedTypeData::Array(_, inner_type) => { - matches!(inner_type.typ, UnresolvedTypeData::FieldElement) - }, + UnresolvedTypeData::Array(_, inner_type) => matches!(inner_type.typ, UnresolvedTypeData::FieldElement), _ => false, } } @@ -53,77 +48,33 @@ fn check_for_compute_note_hash_and_nullifier_definition( pub fn inject_compute_note_hash_and_nullifier( crate_id: &CrateId, context: &mut HirContext, - unresolved_traits_impls: &[UnresolvedTraitImpl], - collected_functions: &mut [UnresolvedFunctions], -) -> Result<(), (MacroError, FileId)> { - // We first fetch modules in this crate which correspond to contracts, along with their file id. - let contract_module_file_ids: Vec<(LocalModuleId, FileId)> = context - .def_map(crate_id) - .expect("ICE: Missing crate in def_map") - .modules() - .iter() - .filter(|(_, module)| module.is_contract) - .map(|(idx, module)| (LocalModuleId(idx), module.location.file)) - .collect(); - - // If the current crate does not contain a contract module we simply skip it. - if contract_module_file_ids.is_empty() { - return Ok(()); - } else if contract_module_file_ids.len() != 1 { - panic!("Found multiple contracts in the same crate"); +) -> 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 + // escape hatch for this mechanism. + // TODO(#4647): improve this diagnosis and error messaging. + if check_for_compute_note_hash_and_nullifier_definition(crate_id, context) { + return Ok(()); + } + + // In order to implement compute_note_hash_and_nullifier, we need to know all of the different note types the + // contract might use. These are the types that are marked as #[aztec(note)]. + let note_types = fetch_notes(context) + .iter() + .map(|(_, note)| note.borrow().name.0.contents.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); + + // And inject the newly created function into the contract. + + // TODO(#4373): We don't have a reasonable location for the source code of this autogenerated function, so we simply + // pass an empty span. This function should not produce errors anyway so this should not matter. + let location = Location::new(Span::empty(0), file_id); + + inject_fn(crate_id, context, func, location, module_id, file_id); } - - let (module_id, file_id) = contract_module_file_ids[0]; - - // If compute_note_hash_and_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 collected_functions.iter().any(|coll_funcs_data| { - check_for_compute_note_hash_and_nullifier_definition(&coll_funcs_data.functions, module_id) - }) { - return Ok(()); - } - - // In order to implement compute_note_hash_and_nullifier, we need to know all of the different note types the - // contract might use. These are the types that implement the NoteInterface trait, which provides the - // get_note_type_id function. - let note_types = fetch_struct_trait_impls(context, unresolved_traits_impls, "NoteInterface"); - - // 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); - - // And inject the newly created function into the contract. - - // TODO(#4373): We don't have a reasonable location for the source code of this autogenerated function, so we simply - // pass an empty span. This function should not produce errors anyway so this should not matter. - let location = Location::new(Span::empty(0), file_id); - - // These are the same things the ModCollector does when collecting functions: we push the function to the - // NodeInterner, declare it in the module (which checks for duplicate definitions), and finally add it to the list - // on collected but unresolved functions. - - let func_id = context.def_interner.push_empty_fn(); - context.def_interner.push_function( - func_id, - &func.def, - ModuleId { krate: *crate_id, local_id: module_id }, - location, - ); - - context.def_map_mut(crate_id).unwrap() - .modules_mut()[module_id.0] - .declare_function( - func.name_ident().clone(), ItemVisibility::Public, func_id - ).expect( - "Failed to declare the autogenerated compute_note_hash_and_nullifier function, likely due to a duplicate definition. See https://github.com/AztecProtocol/aztec-packages/issues/4647." - ); - - collected_functions - .iter_mut() - .find(|fns| fns.file_id == file_id) - .expect("ICE: no functions found in contract file") - .push_fn(module_id, func_id, func.clone()); - Ok(()) } diff --git a/noir/noir-repo/aztec_macros/src/transforms/events.rs b/noir/noir-repo/aztec_macros/src/transforms/events.rs index b77a5821b81..4f2b70453df 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/events.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/events.rs @@ -174,7 +174,7 @@ pub fn transform_events( crate_id: &CrateId, context: &mut HirContext, ) -> Result<(), (AztecMacroError, FileId)> { - for struct_id in collect_crate_structs(crate_id, context) { + for (_, struct_id) in collect_crate_structs(crate_id, context) { let attributes = context.def_interner.struct_attributes(&struct_id); if attributes.iter().any(|attr| is_custom_attribute(attr, "aztec(event)")) { transform_event(struct_id, &mut context.def_interner)?; diff --git a/noir/noir-repo/aztec_macros/src/transforms/note_interface.rs b/noir/noir-repo/aztec_macros/src/transforms/note_interface.rs index 01d0272088b..0514155824e 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/note_interface.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/note_interface.rs @@ -1,7 +1,11 @@ use noirc_errors::Span; use noirc_frontend::{ - parse_program, parser::SortedModule, ItemVisibility, NoirFunction, NoirStruct, PathKind, - TraitImplItem, TypeImpl, UnresolvedTypeData, UnresolvedTypeExpression, + graph::CrateId, + macros_api::{FileId, HirContext, HirExpression, HirLiteral, HirStatement}, + parse_program, + parser::SortedModule, + ItemVisibility, LetStatement, NoirFunction, NoirStruct, PathKind, TraitImplItem, Type, + TypeImpl, UnresolvedTypeData, UnresolvedTypeExpression, }; use regex::Regex; @@ -12,6 +16,7 @@ use crate::{ check_trait_method_implemented, ident, ident_path, is_custom_attribute, make_type, }, errors::AztecMacroError, + hir_utils::{fetch_notes, get_contract_module_data, inject_global}, }, }; @@ -24,7 +29,7 @@ pub fn generate_note_interface_impl(module: &mut SortedModule) -> Result<(), Azt .iter_mut() .filter(|typ| typ.attributes.iter().any(|attr| is_custom_attribute(attr, "aztec(note)"))); - let mut note_properties_structs = vec![]; + let mut structs_to_inject = vec![]; for note_struct in annotated_note_structs { // Look for the NoteInterface trait implementation for the note @@ -80,6 +85,7 @@ pub fn generate_note_interface_impl(module: &mut SortedModule) -> Result<(), Azt )), }), }?; + let note_type_id = note_type_id(¬e_type); // Automatically inject the header field if it's not present let (header_field_name, _) = if let Some(existing_header) = @@ -138,7 +144,7 @@ pub fn generate_note_interface_impl(module: &mut SortedModule) -> Result<(), Azt &header_field_name.0.contents, note_interface_impl_span, )?; - note_properties_structs.push(note_properties_struct); + structs_to_inject.push(note_properties_struct); let note_properties_fn = generate_note_properties_fn( ¬e_type, ¬e_fields, @@ -167,7 +173,7 @@ pub fn generate_note_interface_impl(module: &mut SortedModule) -> Result<(), Azt if !check_trait_method_implemented(trait_impl, "get_note_type_id") { let get_note_type_id_fn = - generate_note_get_type_id(¬e_type, note_interface_impl_span)?; + generate_note_get_type_id(¬e_type_id, note_interface_impl_span)?; trait_impl.items.push(TraitImplItem::Function(get_note_type_id_fn)); } @@ -178,7 +184,7 @@ pub fn generate_note_interface_impl(module: &mut SortedModule) -> Result<(), Azt } } - module.types.extend(note_properties_structs); + module.types.extend(structs_to_inject); Ok(()) } @@ -245,19 +251,16 @@ fn generate_note_set_header( // Automatically generate the note type id getter method. The id itself its calculated as the concatenation // of the conversion of the characters in the note's struct name to unsigned integers. fn generate_note_get_type_id( - note_type: &str, + note_type_id: &str, impl_span: Option, ) -> Result { - // TODO(#4519) Improve automatic note id generation and assignment - let note_id = - note_type.chars().map(|c| (c as u32).to_string()).collect::>().join(""); let function_source = format!( " fn get_note_type_id() -> Field {{ {} }} ", - note_id + note_type_id ) .to_string(); @@ -443,6 +446,34 @@ fn generate_compute_note_content_hash( Ok(noir_fn) } +fn generate_note_exports_global( + note_type: &str, + note_type_id: &str, +) -> Result { + let struct_source = format!( + " + #[abi(notes)] + global {0}_EXPORTS: (Field, str<{1}>) = ({2},\"{0}\"); + ", + note_type, + note_type_id.len(), + note_type_id + ) + .to_string(); + + let (global_ast, errors) = parse_program(&struct_source); + if !errors.is_empty() { + dbg!(errors); + return Err(AztecMacroError::CouldNotImplementNoteInterface { + secondary_message: Some(format!("Failed to parse Noir macro code (struct {}Exports). This is either a bug in the compiler or the Noir macro code", note_type)), + span: None + }); + } + + let mut global_ast = global_ast.into_sorted(); + Ok(global_ast.globals.pop().unwrap()) +} + // Source code generator functions. These utility methods produce Noir code as strings, that are then parsed and added to the AST. fn generate_note_properties_struct_source( @@ -581,3 +612,85 @@ fn generate_note_deserialize_content_source( ) .to_string() } + +// Utility function to generate the note type id as a Field +fn note_type_id(note_type: &str) -> String { + // TODO(#4519) Improve automatic note id generation and assignment + note_type.chars().map(|c| (c as u32).to_string()).collect::>().join("") +} + +pub fn inject_note_exports( + crate_id: &CrateId, + context: &mut HirContext, +) -> Result<(), (AztecMacroError, FileId)> { + if let Some((module_id, file_id)) = get_contract_module_data(context, crate_id) { + let notes = fetch_notes(context); + + for (_, note) in notes { + let func_id = context + .def_interner + .lookup_method( + &Type::Struct(context.def_interner.get_struct(note.borrow().id), vec![]), + note.borrow().id, + "get_note_type_id", + false, + ) + .ok_or(( + AztecMacroError::CouldNotExportStorageLayout { + span: None, + secondary_message: Some(format!( + "Could not retrieve get_note_type_id function for note {}", + note.borrow().name.0.contents + )), + }, + file_id, + ))?; + let init_function = + context.def_interner.function(&func_id).block(&context.def_interner); + let init_function_statement_id = init_function.statements().first().ok_or(( + AztecMacroError::CouldNotExportStorageLayout { + span: None, + secondary_message: Some(format!( + "Could not retrieve note id statement from function for note {}", + note.borrow().name.0.contents + )), + }, + file_id, + ))?; + let note_id_statement = context.def_interner.statement(init_function_statement_id); + + let note_id_value = match note_id_statement { + HirStatement::Expression(expression_id) => { + match context.def_interner.expression(&expression_id) { + HirExpression::Literal(HirLiteral::Integer(value, _)) => Ok(value), + _ => Err(( + AztecMacroError::CouldNotExportStorageLayout { + span: None, + secondary_message: Some( + "note_id statement must be a literal expression".to_string(), + ), + }, + file_id, + )), + } + } + _ => Err(( + AztecMacroError::CouldNotAssignStorageSlots { + secondary_message: Some( + "note_id statement must be an expression".to_string(), + ), + }, + file_id, + )), + }?; + let global = generate_note_exports_global( + ¬e.borrow().name.0.contents, + ¬e_id_value.to_string(), + ) + .map_err(|err| (err, file_id))?; + + inject_global(crate_id, context, global, module_id, file_id); + } + } + Ok(()) +} diff --git a/noir/noir-repo/aztec_macros/src/transforms/storage.rs b/noir/noir-repo/aztec_macros/src/transforms/storage.rs index ab9d8d587ab..0bfb39cbc71 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/storage.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/storage.rs @@ -1,4 +1,4 @@ -use std::borrow::{Borrow, BorrowMut}; +use std::borrow::Borrow; use noirc_errors::Span; use noirc_frontend::{ @@ -7,7 +7,9 @@ use noirc_frontend::{ FieldElement, FileId, HirContext, HirExpression, HirLiteral, HirStatement, NodeInterner, }, node_interner::{TraitId, TraitImplKind}, + parse_program, parser::SortedModule, + token::SecondaryAttribute, BlockExpression, Expression, ExpressionKind, FunctionDefinition, Ident, Literal, NoirFunction, NoirStruct, PathKind, Pattern, StatementKind, Type, TypeImpl, UnresolvedType, UnresolvedTypeData, @@ -21,7 +23,7 @@ use crate::{ make_type, pattern, return_type, variable, variable_path, }, errors::AztecMacroError, - hir_utils::{collect_crate_structs, collect_traits}, + hir_utils::{collect_crate_structs, collect_traits, get_contract_module_data}, }, }; @@ -263,18 +265,51 @@ pub fn assign_storage_slots( context: &mut HirContext, ) -> Result<(), (AztecMacroError, FileId)> { let traits: Vec<_> = collect_traits(context); - for struct_id in collect_crate_structs(crate_id, context) { - let interner: &mut NodeInterner = context.def_interner.borrow_mut(); - let r#struct = interner.get_struct(struct_id); - let file_id = r#struct.borrow().location.file; - let attributes = interner.struct_attributes(&struct_id); - if attributes.iter().any(|attr| is_custom_attribute(attr, "aztec(storage)")) - && r#struct.borrow().id.krate().is_root() + if let Some((_, file_id)) = get_contract_module_data(context, crate_id) { + let maybe_storage_struct = + collect_crate_structs(crate_id, context).iter().find_map(|&(_, struct_id)| { + let r#struct = context.def_interner.get_struct(struct_id); + let attributes = context.def_interner.struct_attributes(&struct_id); + if attributes.iter().any(|attr| is_custom_attribute(attr, "aztec(storage)")) + && r#struct.borrow().id.krate().is_root() + { + Some(r#struct) + } else { + None + } + }); + + let maybe_storage_layout = + context.def_interner.get_all_globals().iter().find_map(|global_info| { + let statement = context.def_interner.get_global_let_statement(global_info.id); + if statement.clone().is_some_and(|stmt| { + stmt.attributes + .iter() + .any(|attr| *attr == SecondaryAttribute::Abi("storage".to_string())) + }) { + let expr = context.def_interner.expression(&statement.unwrap().expression); + match expr { + HirExpression::Constructor(hir_constructor_expression) => { + Some(hir_constructor_expression) + } + _ => None, + } + } else { + None + } + }); + + if let (Some(storage_struct), Some(storage_layout)) = + (maybe_storage_struct, maybe_storage_layout) { - let init_id = interner + let init_id = context + .def_interner .lookup_method( - &Type::Struct(interner.get_struct(struct_id), vec![]), - struct_id, + &Type::Struct( + context.def_interner.get_struct(storage_struct.borrow().id), + vec![], + ), + storage_struct.borrow().id, "init", false, ) @@ -286,28 +321,33 @@ pub fn assign_storage_slots( }, file_id, ))?; - let init_function = interner.function(&init_id).block(interner); + let init_function = + context.def_interner.function(&init_id).block(&context.def_interner); let init_function_statement_id = init_function.statements().first().ok_or(( AztecMacroError::CouldNotAssignStorageSlots { secondary_message: Some("Init storage statement not found".to_string()), }, file_id, ))?; - let storage_constructor_statement = interner.statement(init_function_statement_id); + let storage_constructor_statement = + context.def_interner.statement(init_function_statement_id); let storage_constructor_expression = match storage_constructor_statement { HirStatement::Expression(expression_id) => { - match interner.expression(&expression_id) { - HirExpression::Constructor(hir_constructor_expression) => { - Ok(hir_constructor_expression) - } - _ => Err((AztecMacroError::CouldNotAssignStorageSlots { + match context.def_interner.expression(&expression_id) { + HirExpression::Constructor(hir_constructor_expression) => { + Ok(hir_constructor_expression) + } + _ => Err(( + AztecMacroError::CouldNotAssignStorageSlots { secondary_message: Some( "Storage constructor statement must be a constructor expression" .to_string(), ), - }, file_id)) - } + }, + file_id, + )), + } } _ => Err(( AztecMacroError::CouldNotAssignStorageSlots { @@ -321,9 +361,9 @@ pub fn assign_storage_slots( let mut storage_slot: u64 = 1; for (index, (_, expr_id)) in storage_constructor_expression.fields.iter().enumerate() { - let fields = r#struct.borrow().get_fields(&[]); - let (_, field_type) = fields.get(index).unwrap(); - let new_call_expression = match interner.expression(expr_id) { + let fields = storage_struct.borrow().get_fields(&[]); + let (field_name, field_type) = fields.get(index).unwrap(); + let new_call_expression = match context.def_interner.expression(expr_id) { HirExpression::Call(hir_call_expression) => Ok(hir_call_expression), _ => Err(( AztecMacroError::CouldNotAssignStorageSlots { @@ -336,7 +376,8 @@ pub fn assign_storage_slots( )), }?; - let slot_arg_expression = interner.expression(&new_call_expression.arguments[1]); + let slot_arg_expression = + context.def_interner.expression(&new_call_expression.arguments[1]); let current_storage_slot = match slot_arg_expression { HirExpression::Literal(HirLiteral::Integer(slot, _)) => Ok(slot.to_u128()), @@ -351,22 +392,123 @@ pub fn assign_storage_slots( )), }?; - if current_storage_slot != 0 { - continue; - } + let storage_layout_field = + storage_layout.fields.iter().find(|field| field.0 .0.contents == *field_name); + + let storage_layout_slot_expr_id = + if let Some((_, expr_id)) = storage_layout_field { + let expr = context.def_interner.expression(expr_id); + if let HirExpression::Constructor(storage_layout_field_storable_expr) = expr + { + storage_layout_field_storable_expr.fields.iter().find_map( + |(field, expr_id)| { + if field.0.contents == "slot" { + Some(*expr_id) + } else { + None + } + }, + ) + } else { + None + } + } else { + None + } + .ok_or(( + AztecMacroError::CouldNotAssignStorageSlots { + secondary_message: Some(format!( + "Storage layout field ({}) not found or has an incorrect type", + field_name + )), + }, + file_id, + ))?; + + let new_storage_slot = if current_storage_slot == 0 { + u128::from(storage_slot) + } else { + current_storage_slot + }; + + let type_serialized_len = + get_serialized_length(&traits, field_type, &context.def_interner) + .map_err(|err| (err, file_id))?; - let type_serialized_len = get_serialized_length(&traits, field_type, interner) - .map_err(|err| (err, file_id))?; - interner.update_expression(new_call_expression.arguments[1], |expr| { + context.def_interner.update_expression(new_call_expression.arguments[1], |expr| { *expr = HirExpression::Literal(HirLiteral::Integer( - FieldElement::from(u128::from(storage_slot)), + FieldElement::from(new_storage_slot), false, - )); + )) + }); + + context.def_interner.update_expression(storage_layout_slot_expr_id, |expr| { + *expr = HirExpression::Literal(HirLiteral::Integer( + FieldElement::from(new_storage_slot), + false, + )) }); storage_slot += type_serialized_len; } } } + + Ok(()) +} + +pub fn generate_storage_layout( + module: &mut SortedModule, + storage_struct_name: String, +) -> Result<(), AztecMacroError> { + let definition = module + .types + .iter() + .find(|r#struct| r#struct.name.0.contents == *storage_struct_name) + .unwrap(); + + let mut generic_args = vec![]; + let mut storable_fields = vec![]; + let mut storable_fields_impl = vec![]; + + definition.fields.iter().enumerate().for_each(|(index, (field_ident, field_type))| { + storable_fields.push(format!("{}: dep::aztec::prelude::Storable", field_ident, index)); + generic_args.push(format!("N{}", index)); + storable_fields_impl.push(format!( + "{}: dep::aztec::prelude::Storable {{ slot: 0, typ: \"{}\" }}", + field_ident, + field_type.to_string().replace("plain::", "") + )); + }); + + let storage_fields_source = format!( + " + struct StorageLayout<{}> {{ + {} + }} + + #[abi(storage)] + global STORAGE_LAYOUT = StorageLayout {{ + {} + }}; + ", + generic_args.join(", "), + storable_fields.join(",\n"), + storable_fields_impl.join(",\n") + ); + + let (struct_ast, errors) = parse_program(&storage_fields_source); + if !errors.is_empty() { + dbg!(errors); + return Err(AztecMacroError::CouldNotImplementNoteInterface { + secondary_message: Some("Failed to parse Noir macro code (struct StorageLayout). This is either a bug in the compiler or the Noir macro code".to_string()), + span: None + }); + } + + let mut struct_ast = struct_ast.into_sorted(); + module.types.push(struct_ast.types.pop().unwrap()); + module.globals.push(struct_ast.globals.pop().unwrap()); + Ok(()) } diff --git a/noir/noir-repo/aztec_macros/src/utils/errors.rs b/noir/noir-repo/aztec_macros/src/utils/errors.rs index 509322b3db2..e3a3db87a3d 100644 --- a/noir/noir-repo/aztec_macros/src/utils/errors.rs +++ b/noir/noir-repo/aztec_macros/src/utils/errors.rs @@ -12,6 +12,7 @@ pub enum AztecMacroError { CouldNotAssignStorageSlots { secondary_message: Option }, CouldNotImplementNoteInterface { span: Option, secondary_message: Option }, MultipleStorageDefinitions { span: Option }, + CouldNotExportStorageLayout { span: Option, secondary_message: Option }, EventError { span: Span, message: String }, UnsupportedAttributes { span: Span, secondary_message: Option }, } @@ -54,6 +55,11 @@ impl From for MacroError { secondary_message: None, span, }, + AztecMacroError::CouldNotExportStorageLayout { secondary_message, span } => MacroError { + primary_message: "Could not generate and export storage layout".to_string(), + secondary_message, + span, + }, AztecMacroError::EventError { span, message } => MacroError { primary_message: message, secondary_message: None, diff --git a/noir/noir-repo/aztec_macros/src/utils/hir_utils.rs b/noir/noir-repo/aztec_macros/src/utils/hir_utils.rs index f31a0584261..c4414e6419b 100644 --- a/noir/noir-repo/aztec_macros/src/utils/hir_utils.rs +++ b/noir/noir-repo/aztec_macros/src/utils/hir_utils.rs @@ -1,22 +1,43 @@ use iter_extended::vecmap; +use noirc_errors::Location; use noirc_frontend::{ graph::CrateId, - hir::def_collector::dc_crate::UnresolvedTraitImpl, - macros_api::{HirContext, ModuleDefId, StructId}, - node_interner::{TraitId, TraitImplId}, - Signedness, Type, UnresolvedTypeData, + hir::{ + def_map::{LocalModuleId, ModuleId}, + resolution::{path_resolver::StandardPathResolver, resolver::Resolver}, + type_check::type_check_func, + }, + macros_api::{FileId, HirContext, ModuleDefId, StructId}, + node_interner::{FuncId, TraitId}, + ItemVisibility, LetStatement, NoirFunction, Shared, Signedness, StructType, Type, }; -pub fn collect_crate_structs(crate_id: &CrateId, context: &HirContext) -> Vec { +use super::ast_utils::is_custom_attribute; + +pub fn collect_crate_structs(crate_id: &CrateId, context: &HirContext) -> Vec<(String, StructId)> { context .def_map(crate_id) .expect("ICE: Missing crate in def_map") .modules() .iter() .flat_map(|(_, module)| { - module.type_definitions().filter_map(|typ| { + module.type_definitions().filter_map(move |typ| { if let ModuleDefId::TypeId(struct_id) = typ { - Some(struct_id) + let module_id = struct_id.module_id(); + let path = + context.fully_qualified_struct_path(context.root_crate_id(), struct_id); + let path = if path.contains("::") { + let prefix = if &module_id.krate == context.root_crate_id() { + "crate" + } else { + "dep" + }; + format!("{}::{}", prefix, path) + } else { + path + }; + + Some((path, struct_id)) } else { None } @@ -25,6 +46,16 @@ pub fn collect_crate_structs(crate_id: &CrateId, context: &HirContext) -> Vec Vec { + context + .def_map(crate_id) + .expect("ICE: Missing crate in def_map") + .modules() + .iter() + .flat_map(|(_, module)| module.value_definitions().filter_map(|id| id.as_function())) + .collect() +} + pub fn collect_traits(context: &HirContext) -> Vec { let crates = context.crates(); crates @@ -32,8 +63,8 @@ pub fn collect_traits(context: &HirContext) -> Vec { .flatten() .flat_map(|module| { module.type_definitions().filter_map(|typ| { - if let ModuleDefId::TraitId(struct_id) = typ { - Some(struct_id) + if let ModuleDefId::TraitId(trait_id) = typ { + Some(trait_id) } else { None } @@ -69,50 +100,127 @@ pub fn signature_of_type(typ: &Type) -> String { } } -// Fetches the name of all structs that implement trait_name, both in the current crate and all of its dependencies. -pub fn fetch_struct_trait_impls( - context: &mut HirContext, - unresolved_traits_impls: &[UnresolvedTraitImpl], - trait_name: &str, -) -> Vec { - let mut struct_typenames: Vec = Vec::new(); - - // These structs can be declared in either external crates or the current one. External crates that contain - // dependencies have already been processed and resolved, but are available here via the NodeInterner. Note that - // crates on which the current crate does not depend on may not have been processed, and will be ignored. - for trait_impl_id in 0..context.def_interner.next_trait_impl_id().0 { - let trait_impl = &context.def_interner.get_trait_implementation(TraitImplId(trait_impl_id)); - - if trait_impl.borrow().ident.0.contents == *trait_name { - if let Type::Struct(s, _) = &trait_impl.borrow().typ { - struct_typenames.push(s.borrow().name.0.contents.clone()); +// Fetches the name of all structs tagged as #[aztec(note)] in a given crate +pub fn fetch_crate_notes( + context: &HirContext, + crate_id: &CrateId, +) -> Vec<(String, Shared)> { + collect_crate_structs(crate_id, context) + .iter() + .filter_map(|(path, struct_id)| { + let r#struct = context.def_interner.get_struct(*struct_id); + let attributes = context.def_interner.struct_attributes(struct_id); + if attributes.iter().any(|attr| is_custom_attribute(attr, "aztec(note)")) { + Some((path.clone(), r#struct)) } else { - panic!("Found impl for {} on non-Struct", trait_name); + None } - } + }) + .collect() +} + +// Fetches the name of all structs tagged as #[aztec(note)], both in the current crate and all of its dependencies. +pub fn fetch_notes(context: &HirContext) -> Vec<(String, Shared)> { + context.crates().flat_map(|crate_id| fetch_crate_notes(context, &crate_id)).collect() +} + +pub fn get_contract_module_data( + context: &mut HirContext, + crate_id: &CrateId, +) -> Option<(LocalModuleId, FileId)> { + // We first fetch modules in this crate which correspond to contracts, along with their file id. + let contract_module_file_ids: Vec<(LocalModuleId, FileId)> = context + .def_map(crate_id) + .expect("ICE: Missing crate in def_map") + .modules() + .iter() + .filter(|(_, module)| module.is_contract) + .map(|(idx, module)| (LocalModuleId(idx), module.location.file)) + .collect(); + + // If the current crate does not contain a contract module we simply skip it. More than 1 contract in a crate is forbidden by the compiler + if contract_module_file_ids.is_empty() { + return None; } - // This crate's traits and impls have not yet been resolved, so we look for impls in unresolved_trait_impls. - struct_typenames.extend( - unresolved_traits_impls - .iter() - .filter(|trait_impl| { - trait_impl - .trait_path - .segments - .last() - .expect("ICE: empty trait_impl path") - .0 - .contents - == *trait_name - }) - .filter_map(|trait_impl| match &trait_impl.object_type.typ { - UnresolvedTypeData::Named(path, _, _) => { - Some(path.segments.last().unwrap().0.contents.clone()) - } - _ => None, - }), + Some(contract_module_file_ids[0]) +} + +pub fn inject_fn( + crate_id: &CrateId, + context: &mut HirContext, + func: NoirFunction, + location: Location, + module_id: LocalModuleId, + file_id: FileId, +) { + let func_id = context.def_interner.push_empty_fn(); + context.def_interner.push_function( + func_id, + &func.def, + ModuleId { krate: *crate_id, local_id: module_id }, + location, + ); + + context.def_map_mut(crate_id).unwrap().modules_mut()[module_id.0] + .declare_function(func.name_ident().clone(), ItemVisibility::Public, func_id) + .unwrap_or_else(|_| { + panic!( + "Failed to declare autogenerated {} function, likely due to a duplicate definition", + func.name() + ) + }); + + let def_maps = &mut context.def_maps; + + let path_resolver = + StandardPathResolver::new(ModuleId { local_id: module_id, krate: *crate_id }); + + let resolver = Resolver::new(&mut context.def_interner, &path_resolver, def_maps, file_id); + + let (hir_func, meta, _) = resolver.resolve_function(func, func_id); + + context.def_interner.push_fn_meta(meta, func_id); + context.def_interner.update_fn(func_id, hir_func); + + type_check_func(&mut context.def_interner, func_id); +} + +pub fn inject_global( + crate_id: &CrateId, + context: &mut HirContext, + global: LetStatement, + module_id: LocalModuleId, + file_id: FileId, +) { + let name = global.pattern.name_ident().clone(); + + let global_id = context.def_interner.push_empty_global( + name.clone(), + module_id, + file_id, + global.attributes.clone(), ); - struct_typenames + // Add the statement to the scope so its path can be looked up later + context.def_map_mut(crate_id).unwrap().modules_mut()[module_id.0] + .declare_global(name, global_id) + .unwrap_or_else(|(name, _)| { + panic!( + "Failed to declare autogenerated {} global, likely due to a duplicate definition", + name + ) + }); + + let def_maps = &mut context.def_maps; + + let path_resolver = + StandardPathResolver::new(ModuleId { local_id: module_id, krate: *crate_id }); + + let mut resolver = Resolver::new(&mut context.def_interner, &path_resolver, def_maps, file_id); + + let hir_stmt = resolver.resolve_global_let(global, global_id); + + let statement_id = context.def_interner.get_global(global_id).let_statement; + context.def_interner.replace_statement(statement_id, hir_stmt); } diff --git a/noir/noir-repo/compiler/noirc_driver/src/abi_gen.rs b/noir/noir-repo/compiler/noirc_driver/src/abi_gen.rs index 516c8a53cb6..86f10818dbc 100644 --- a/noir/noir-repo/compiler/noirc_driver/src/abi_gen.rs +++ b/noir/noir-repo/compiler/noirc_driver/src/abi_gen.rs @@ -112,6 +112,15 @@ fn collapse_ranges(witnesses: &[Witness]) -> Vec> { pub(super) fn value_from_hir_expression(context: &Context, expression: HirExpression) -> AbiValue { match expression { + HirExpression::Tuple(expr_ids) => { + let fields = expr_ids + .iter() + .map(|expr_id| { + value_from_hir_expression(context, context.def_interner.expression(expr_id)) + }) + .collect(); + AbiValue::Tuple { fields } + } HirExpression::Constructor(constructor) => { let fields = constructor .fields @@ -151,7 +160,7 @@ pub(super) fn value_from_hir_expression(context: &Context, expression: HirExpres } _ => unreachable!("Literal cannot be used in the abi"), }, - _ => unreachable!("Type cannot be used in the abi"), + _ => unreachable!("Type cannot be used in the abi {:?}", expression), } } diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs index 90aa4baee7c..463b8a4b329 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/def_collector/dc_crate.rs @@ -256,20 +256,6 @@ impl DefCollector { // Add the current crate to the collection of DefMaps context.def_maps.insert(crate_id, def_collector.def_map); - // TODO(#4653): generalize this function - for macro_processor in macro_processors { - macro_processor - .process_collected_defs( - &crate_id, - context, - &def_collector.collected_traits_impls, - &mut def_collector.collected_functions, - ) - .unwrap_or_else(|(macro_err, file_id)| { - errors.push((macro_err.into(), file_id)); - }); - } - inject_prelude(crate_id, context, crate_root, &mut def_collector.collected_imports); for submodule in submodules { inject_prelude( diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/mod.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/mod.rs index 00bcb0cdebf..727a6596df1 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/mod.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/mod.rs @@ -26,7 +26,7 @@ pub type ParsedFiles = HashMap)>; pub struct Context<'file_manager, 'parsed_files> { pub def_interner: NodeInterner, pub crate_graph: CrateGraph, - pub(crate) def_maps: BTreeMap, + pub def_maps: BTreeMap, // In the WASM context, we take ownership of the file manager, // which is why this needs to be a Cow. In all use-cases, the file manager // is read-only however, once it has been passed to the Context. diff --git a/noir/noir-repo/compiler/noirc_frontend/src/lib.rs b/noir/noir-repo/compiler/noirc_frontend/src/lib.rs index 6ce6f4325e4..93d7960faf5 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/lib.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/lib.rs @@ -45,7 +45,6 @@ pub mod macros_api { pub use noirc_errors::Span; pub use crate::graph::CrateId; - use crate::hir::def_collector::dc_crate::{UnresolvedFunctions, UnresolvedTraitImpl}; pub use crate::hir::def_collector::errors::MacroError; pub use crate::hir_def::expr::{HirExpression, HirLiteral}; pub use crate::hir_def::stmt::HirStatement; @@ -76,15 +75,6 @@ pub mod macros_api { context: &HirContext, ) -> Result; - // TODO(#4653): generalize this function - fn process_collected_defs( - &self, - _crate_id: &CrateId, - _context: &mut HirContext, - _collected_trait_impls: &[UnresolvedTraitImpl], - _collected_functions: &mut [UnresolvedFunctions], - ) -> Result<(), (MacroError, FileId)>; - /// Function to manipulate the AST after type checking has been completed. /// The AST after type checking has been done is called the HIR. fn process_typed_ast( diff --git a/yarn-project/aztec.js/src/contract/contract_base.ts b/yarn-project/aztec.js/src/contract/contract_base.ts index a49a7205c13..5758973849e 100644 --- a/yarn-project/aztec.js/src/contract/contract_base.ts +++ b/yarn-project/aztec.js/src/contract/contract_base.ts @@ -1,4 +1,4 @@ -import { computePartialAddress } from '@aztec/circuits.js'; +import { type Fr, computePartialAddress } from '@aztec/circuits.js'; import { type ContractArtifact, type FunctionArtifact, FunctionSelector } from '@aztec/foundation/abi'; import { type ContractInstanceWithAddress } from '@aztec/types/contracts'; @@ -16,6 +16,48 @@ export type ContractMethod = ((...args: any[]) => ContractFunctionInteraction) & readonly selector: FunctionSelector; }; +/** + * Type representing a field layout in the storage of a contract. + */ +type FieldLayout = { + /** + * Slot in which the field is stored. + */ + slot: Fr; + /** + * Type being stored at the slot + */ + typ: string; +}; + +/** + * Type representing a note in use in the contract. + */ +type ContractNote = { + /** + * Note identifier + */ + id: Fr; + /** + * Type of the note + */ + typ: string; +}; + +/** + * Type representing the storage layout of a contract. + */ +export type ContractStorageLayout = { + [K in T]: FieldLayout; +}; + +/** + * Type representing the notes used in a contract. + */ +export type ContractNotes = { + [K in T]: ContractNote; +}; + /** * Abstract implementation of a contract extended by the Contract class and generated contract types. */ diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 646c5488bf9..e7cc7c0ba20 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -25,6 +25,8 @@ export { Contract, ContractBase, ContractMethod, + ContractStorageLayout, + ContractNotes, SentTx, BatchCall, DeployMethod, diff --git a/yarn-project/circuits.js/src/contract/artifact_hash.test.ts b/yarn-project/circuits.js/src/contract/artifact_hash.test.ts index 70a1ea58c18..e01b9484f21 100644 --- a/yarn-project/circuits.js/src/contract/artifact_hash.test.ts +++ b/yarn-project/circuits.js/src/contract/artifact_hash.test.ts @@ -5,7 +5,7 @@ describe('ArtifactHash', () => { it('calculates the artifact hash', () => { const artifact = getBenchmarkContractArtifact(); expect(computeArtifactHash(artifact).toString()).toMatchInlineSnapshot( - `"0x0698cf658c2b1672eb0bbb6df8fc6760e8c34af4c7779f8206dab211b3b28814"`, + `"0x011603d7f02ebec628e8f1b2458edff811648ea3af5399cec32302aab6217b26"`, ); }); }); 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 170eb6c4d86..2ec02208400 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 @@ -98,11 +98,15 @@ describe('e2e_2_pxes', () => { const receipt = await contract.methods.mint_private(balance, secretHash).send().wait(); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(balance), secretHash]); - const extendedNote = new ExtendedNote(note, recipient, contract.address, storageSlot, noteTypeId, receipt.txHash); + const extendedNote = new ExtendedNote( + note, + recipient, + contract.address, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + receipt.txHash, + ); await pxe.addNote(extendedNote); await contract.methods.redeem_shield(recipient, balance, secret).send().wait(); diff --git a/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts index 1f44365bfcd..7fe2c4e887f 100644 --- a/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_blacklist_token_contract.test.ts @@ -85,15 +85,13 @@ describe('e2e_blacklist_token_contract', () => { }; const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const storageSlot = new Fr(4); // The storage slot of `pending_shields` is 4. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote const note = new Note([new Fr(amount), secretHash]); const extendedNote = new ExtendedNote( note, wallets[accountIndex].getAddress(), asset.address, - storageSlot, - noteTypeId, + TokenBlacklistContract.storage.pending_shields.slot, + TokenBlacklistContract.notes.TransparentNote.id, txHash, ); await wallets[accountIndex].addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts index 20905605d68..01127055f91 100644 --- a/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts +++ b/yarn-project/end-to-end/src/e2e_cheat_codes.test.ts @@ -223,21 +223,23 @@ describe('e2e_cheat_codes', () => { // docs:start:pxe_add_note const note = new Note([new Fr(mintAmount), secretHash]); - const pendingShieldStorageSlot = new Fr(5n); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote const extendedNote = new ExtendedNote( note, admin.address, token.address, - pendingShieldStorageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); // docs:end:pxe_add_note // check if note was added to pending shield: - const notes = await cc.aztec.loadPrivate(admin.address, token.address, pendingShieldStorageSlot); + const notes = await cc.aztec.loadPrivate( + admin.address, + token.address, + TokenContract.storage.pending_shields.slot, + ); const values = notes.map(note => note.items[0]); const balance = values.reduce((sum, current) => sum + current.toBigInt(), 0n); expect(balance).toEqual(mintAmount); diff --git a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts index 1556d2b40ce..fd47b6b9515 100644 --- a/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts +++ b/yarn-project/end-to-end/src/e2e_crowdfunding_and_claim.test.ts @@ -64,10 +64,15 @@ describe('e2e_crowdfunding_and_claim', () => { txHash: TxHash, address: AztecAddress, ) => { - const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote(note, wallet.getAddress(), address, storageSlot, noteTypeId, txHash); + const extendedNote = new ExtendedNote( + note, + wallet.getAddress(), + address, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + txHash, + ); await wallet.addNote(extendedNote); }; diff --git a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts index c8f5b922e9a..0669466aae1 100644 --- a/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_escrow_contract.test.ts @@ -20,9 +20,6 @@ import { TokenContract } from '@aztec/noir-contracts.js/Token'; import { setup } from './fixtures/utils.js'; describe('e2e_escrow_contract', () => { - const pendingShieldsStorageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - let pxe: PXE; let wallet: AccountWallet; let recipientWallet: AccountWallet; @@ -74,8 +71,8 @@ describe('e2e_escrow_contract', () => { note, owner, token.address, - pendingShieldsStorageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); @@ -125,8 +122,8 @@ describe('e2e_escrow_contract', () => { note, owner, token.address, - pendingShieldsStorageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts index 99e63546a55..c29e101fe5a 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -662,16 +662,13 @@ describe('e2e_fees', () => { }; const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(amount), secretHash]); const extendedNote = new ExtendedNote( note, wallets[accountIndex].getAddress(), bananaCoin.address, - storageSlot, - noteTypeId, + BananaCoin.storage.pending_shields.slot, + BananaCoin.notes.TransparentNote.id, txHash, ); await wallets[accountIndex].addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts index 9f7f58c0525..b18131fdcce 100644 --- a/yarn-project/end-to-end/src/e2e_lending_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_lending_contract.test.ts @@ -102,17 +102,14 @@ describe('e2e_lending_contract', () => { const b = asset.methods.mint_private(mintAmount, secretHash).send(); await Promise.all([a, b].map(tx => tx.wait())); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(mintAmount), secretHash]); const txHash = await b.getTxHash(); const extendedNote = new ExtendedNote( note, wallet.getAddress(), asset.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, txHash, ); await wallet.addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts index 921cb5c321b..10fa2c76cda 100644 --- a/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts +++ b/yarn-project/end-to-end/src/e2e_multiple_accounts_1_enc_key.test.ts @@ -63,16 +63,13 @@ describe('e2e_multiple_accounts_1_enc_key', () => { const receipt = await token.methods.mint_private(initialBalance, secretHash).send().wait(); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(initialBalance), secretHash]); const extendedNote = new ExtendedNote( note, accounts[0].address, token.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts b/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts index cc9fb6f70b7..65a56eb1d1b 100644 --- a/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts +++ b/yarn-project/end-to-end/src/e2e_non_contract_account.test.ts @@ -80,15 +80,13 @@ describe('e2e_non_contract_account', () => { // Add the note const note = new Note([new Fr(value)]); - const storageSlot = new Fr(1); - const noteTypeId = new Fr(7010510110810078111116101n); // FieldNote const extendedNote = new ExtendedNote( note, wallet.getCompleteAddress().address, contract.address, - storageSlot, - noteTypeId, + TestContract.storage.example_constant.slot, + TestContract.notes.FieldNote.id, txHash, ); await wallet.addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/e2e_note_getter.test.ts b/yarn-project/end-to-end/src/e2e_note_getter.test.ts index 48320d81ac9..27e6be2c03a 100644 --- a/yarn-project/end-to-end/src/e2e_note_getter.test.ts +++ b/yarn-project/end-to-end/src/e2e_note_getter.test.ts @@ -159,7 +159,7 @@ describe('e2e_note_getter', () => { const VALUE = 5; // To prevent tests from interacting with one another, we'll have each use a different storage slot. - let storageSlot: number = 2; + let storageSlot = TestContract.storage.example_set.slot.toNumber(); beforeEach(() => { storageSlot += 1; diff --git a/yarn-project/end-to-end/src/e2e_persistence.test.ts b/yarn-project/end-to-end/src/e2e_persistence.test.ts index a4a6d216181..cc44b29499f 100644 --- a/yarn-project/end-to-end/src/e2e_persistence.test.ts +++ b/yarn-project/end-to-end/src/e2e_persistence.test.ts @@ -330,12 +330,14 @@ async function addPendingShieldNoteToPXE( secretHash: Fr, txHash: TxHash, ) { - // The storage slot of `pending_shields` is 5. - // TODO AlexG, this feels brittle - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(amount), secretHash]); - const extendedNote = new ExtendedNote(note, wallet.getAddress(), asset, storageSlot, noteTypeId, txHash); + const extendedNote = new ExtendedNote( + note, + wallet.getAddress(), + asset, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + txHash, + ); await wallet.addNote(extendedNote); } diff --git a/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts b/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts index 389a8f2b58e..28416e0d248 100644 --- a/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts +++ b/yarn-project/end-to-end/src/e2e_sandbox_example.test.ts @@ -76,12 +76,16 @@ describe('e2e_sandbox_example', () => { const receipt = await tokenContractAlice.methods.mint_private(initialSupply, aliceSecretHash).send().wait(); // Add the newly created "pending shield" note to PXE - const pendingShieldsStorageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(initialSupply), aliceSecretHash]); await pxe.addNote( - new ExtendedNote(note, alice, contract.address, pendingShieldsStorageSlot, noteTypeId, receipt.txHash), + new ExtendedNote( + note, + alice, + contract.address, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + receipt.txHash, + ), ); // Make the tokens spendable by redeeming them using the secret (converts the "pending shield note" created above @@ -153,8 +157,8 @@ describe('e2e_sandbox_example', () => { bobPendingShield, bob, contract.address, - pendingShieldsStorageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, mintPrivateReceipt.txHash, ), ); diff --git a/yarn-project/end-to-end/src/e2e_token_contract.test.ts b/yarn-project/end-to-end/src/e2e_token_contract.test.ts index a79c47703fb..22e2ca2bd9e 100644 --- a/yarn-project/end-to-end/src/e2e_token_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_token_contract.test.ts @@ -37,16 +37,13 @@ describe('e2e_token_contract', () => { let tokenSim: TokenSimulator; const addPendingShieldNoteToPXE = async (accountIndex: number, amount: bigint, secretHash: Fr, txHash: TxHash) => { - const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(amount), secretHash]); const extendedNote = new ExtendedNote( note, wallets[accountIndex].getAddress(), asset.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, txHash, ); await wallets[accountIndex].addNote(extendedNote); diff --git a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts index 4e3ee9f7a7c..8cde4b58e96 100644 --- a/yarn-project/end-to-end/src/guides/dapp_testing.test.ts +++ b/yarn-project/end-to-end/src/guides/dapp_testing.test.ts @@ -50,16 +50,13 @@ describe('guides/dapp/testing', () => { const secretHash = computeMessageSecretHash(secret); const receipt = await token.methods.mint_private(mintAmount, secretHash).send().wait(); - const storageSlot = new Fr(5); // The storage slot of `pending_shields` is 5. - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(mintAmount), secretHash]); const extendedNote = new ExtendedNote( note, recipientAddress, token.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); @@ -94,16 +91,13 @@ describe('guides/dapp/testing', () => { const secretHash = computeMessageSecretHash(secret); const receipt = await token.methods.mint_private(mintAmount, secretHash).send().wait(); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(mintAmount), secretHash]); const extendedNote = new ExtendedNote( note, recipientAddress, token.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); @@ -159,16 +153,13 @@ describe('guides/dapp/testing', () => { const secretHash = computeMessageSecretHash(secret); const receipt = await token.methods.mint_private(100n, secretHash).send().wait(); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(mintAmount), secretHash]); const extendedNote = new ExtendedNote( note, ownerAddress, token.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, receipt.txHash, ); await pxe.addNote(extendedNote); @@ -177,8 +168,8 @@ describe('guides/dapp/testing', () => { // docs:start:calc-slot cheats = CheatCodes.create(ETHEREUM_HOST, pxe); - // The balances mapping is defined on storage slot 3 and is indexed by user address - ownerSlot = cheats.aztec.computeSlotInMap(3n, ownerAddress); + // The balances mapping is indexed by user address + ownerSlot = cheats.aztec.computeSlotInMap(TokenContract.storage.balances.slot, ownerAddress); // docs:end:calc-slot }, 90_000); diff --git a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts index 185455ce4cd..7f7237b6da6 100644 --- a/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts +++ b/yarn-project/end-to-end/src/guides/writing_an_account_contract.test.ts @@ -73,11 +73,15 @@ describe('guides/writing_an_account_contract', () => { const mintAmount = 50n; const receipt = await token.methods.mint_private(mintAmount, secretHash).send().wait(); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote - const note = new Note([new Fr(mintAmount), secretHash]); - const extendedNote = new ExtendedNote(note, address, token.address, storageSlot, noteTypeId, receipt.txHash); + const extendedNote = new ExtendedNote( + note, + address, + token.address, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, + receipt.txHash, + ); await pxe.addNote(extendedNote); await token.methods.redeem_shield({ address }, mintAmount, secret).send().wait(); diff --git a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts index 78bfdb7ee73..c7ba3f88a82 100644 --- a/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/cross_chain_test_harness.ts @@ -423,15 +423,13 @@ export class CrossChainTestHarness { async addPendingShieldNoteToPXE(shieldAmount: bigint, secretHash: Fr, txHash: TxHash) { this.logger('Adding note to PXE'); - const storageSlot = new Fr(5); - const noteTypeId = new Fr(84114971101151129711410111011678111116101n); // TransparentNote const note = new Note([new Fr(shieldAmount), secretHash]); const extendedNote = new ExtendedNote( note, this.ownerAddress, this.l2Token.address, - storageSlot, - noteTypeId, + TokenContract.storage.pending_shields.slot, + TokenContract.notes.TransparentNote.id, txHash, ); await this.pxeService.addNote(extendedNote); diff --git a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts index 1a77f8916cb..da5e9cd6e8f 100644 --- a/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts +++ b/yarn-project/noir-compiler/src/contract-interface-gen/typescript.ts @@ -1,7 +1,12 @@ import { type ABIParameter, + type BasicValue, type ContractArtifact, type FunctionArtifact, + type IntegerValue, + type StructValue, + type TupleValue, + type TypedStructFieldValue, getDefaultInitializer, isAztecAddressStruct, isEthAddressStruct, @@ -180,6 +185,69 @@ function generateAbiStatement(name: string, artifactImportPath: string) { return stmts.join('\n'); } +/** + * Generates a getter for the contract's storage layout. + * @param input - The contract artifact. + */ +function generateStorageLayoutGetter(input: ContractArtifact) { + const storage = input.outputs.globals.storage ? (input.outputs.globals.storage[0] as StructValue) : { fields: [] }; + const storageFields = storage.fields as TypedStructFieldValue[]; + const storageFieldsUnionType = storageFields.map(f => `'${f.name}'`).join(' | '); + const layout = storageFields + .map( + ({ + name, + value: { + fields: [slot, typ], + }, + }) => + `${name}: { + slot: new Fr(${(slot.value as IntegerValue).value}n), + typ: "${(typ.value as BasicValue<'string', string>).value}", + } + `, + ) + .join(',\n'); + return storageFields.length > 0 + ? ` + public static get storage(): ContractStorageLayout<${storageFieldsUnionType}> { + return { + ${layout} + } as ContractStorageLayout<${storageFieldsUnionType}>; + } + ` + : ''; +} + +/** + * Generates a getter for the contract notes + * @param input - The contract artifact. + */ +function generateNotesGetter(input: ContractArtifact) { + const notes = input.outputs.globals.notes ? (input.outputs.globals.notes as TupleValue[]) : []; + const notesUnionType = notes.map(n => `'${(n.fields[1] as BasicValue<'string', string>).value}'`).join(' | '); + + const noteMetadata = notes + .map( + ({ fields: [id, typ] }) => + `${(typ as BasicValue<'string', string>).value}: { + id: new Fr(${(id as IntegerValue).value}n), + } + `, + ) + .join(',\n'); + return notes.length > 0 + ? ` + public static get notes(): ContractNotes<${notesUnionType}> { + const notes = this.artifact.outputs.globals.notes ? (this.artifact.outputs.globals.notes as any) : []; + return { + ${noteMetadata} + } as ContractNotes<${notesUnionType}>; + } + ` + : ''; +} + /** * Generates the typescript code to represent a contract. * @param input - The compiled Noir artifact. @@ -193,6 +261,8 @@ export function generateTypescriptContractInterface(input: ContractArtifact, art const at = artifactImportPath && generateAt(input.name); const artifactStatement = artifactImportPath && generateAbiStatement(input.name, artifactImportPath); const artifactGetter = artifactImportPath && generateArtifactGetter(input.name); + const storageLayoutGetter = artifactImportPath && generateStorageLayoutGetter(input); + const notesGetter = artifactImportPath && generateNotesGetter(input); return ` /* Autogenerated file, do not edit! */ @@ -208,6 +278,8 @@ import { ContractFunctionInteraction, ContractInstanceWithAddress, ContractMethod, + ContractStorageLayout, + ContractNotes, DeployMethod, EthAddress, EthAddressLike, @@ -235,6 +307,10 @@ export class ${input.name}Contract extends ContractBase { ${artifactGetter} + ${storageLayoutGetter} + + ${notesGetter} + /** Type-safe wrappers for the public methods exposed by the contract. */ public methods!: { ${methods.join('\n')}