diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index 1ca5f170988b..15bf1d33b2af 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,51 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### Scope enforcement for private state access (TXE and PXE) + +Scope enforcement is now active across both TXE (test environment) and PXE (client). Previously, private execution could implicitly access any account's keys and notes. Now, only the caller (`from`) address is in scope by default, and accessing another address's private state requires explicitly granting scope. + +#### Noir developers (TXE) + +TXE now enforces scope isolation, matching PXE behavior. During private execution, only the caller's keys and notes are accessible. If a Noir test accesses private state of an address other than `from`, it will fail. When `from` is the zero address, scopes are empty (deny-all). + +If your TXE tests fail with key or note access errors, ensure the test is calling from the correct address, or restructure the test to match the expected access pattern. + +#### Aztec.js developers (PXE/Wallet) + +The wallet now passes scopes to PXE, and only the `from` address is in scope by default. Auto-expansion of scopes for nested calls to registered accounts has been removed. A new `additionalScopes` option is available on `send()`, `simulate()`, and `deploy()` for cases where private execution needs access to another address's keys or notes. + +**When do you need `additionalScopes`?** + +1. **Deploying contracts whose constructor initializes private storage** (e.g., account contracts, or any contract using `SinglePrivateImmutable`/`SinglePrivateMutable` in the constructor). The contract's own address must be in scope so its nullifier key is accessible during initialization. + +2. **Operations that access another contract's private state** (e.g., withdrawing from an escrow contract that nullifies the contract's own token notes). + +``` + +**Example: deploying a contract with private storage (e.g., `PrivateToken`)** + +```diff + const tokenDeployment = PrivateTokenContract.deployWithPublicKeys( + tokenPublicKeys, wallet, initialBalance, sender, + ); + const tokenInstance = await tokenDeployment.getInstance(); + await wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey); + const token = await tokenDeployment.send({ + from: sender, ++ additionalScopes: [tokenInstance.address], + }); +``` + +**Example: withdrawing from an escrow contract** + +```diff + await escrowContract.methods + .withdraw(token.address, amount, recipient) +- .send({ from: owner }); ++ .send({ from: owner, additionalScopes: [escrowContract.address] }); +``` + ### `simulateUtility` renamed to `executeUtility` The `simulateUtility` method and related types have been renamed to `executeUtility` across the entire stack to better reflect that utility functions are executed, not simulated. diff --git a/noir-projects/aztec-nr/aztec/src/contract_self.nr b/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_private.nr similarity index 70% rename from noir-projects/aztec-nr/aztec/src/contract_self.nr rename to noir-projects/aztec-nr/aztec/src/contract_self/contract_self_private.nr index 69de9b4e2f6a..6065428f97c1 100644 --- a/noir-projects/aztec-nr/aztec/src/contract_self.nr +++ b/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_private.nr @@ -1,32 +1,23 @@ -//! The `self` contract value. +//! The `self` contract value for private execution contexts. use crate::{ - context::{ - calls::{PrivateCall, PrivateStaticCall, PublicCall, PublicStaticCall}, - PrivateContext, - PublicContext, - UtilityContext, - }, - event::{ - event_emission::{emit_event_in_private, emit_event_in_public}, - event_interface::EventInterface, - EventMessage, - }, + context::{calls::{PrivateCall, PrivateStaticCall, PublicCall, PublicStaticCall}, PrivateContext}, + event::{event_emission::emit_event_in_private, event_interface::EventInterface, EventMessage}, }; use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; -/// Core interface for interacting with aztec-nr contract features. +/// Core interface for interacting with aztec-nr contract features in private execution contexts. /// /// This struct is automatically injected into every [`external`](crate::macros::functions::external) and -/// [`internal`](crate::macros::functions::internal) contract function by the Aztec macro system and is accessible -/// through the `self` variable. +/// [`internal`](crate::macros::functions::internal) contract function marked with `"private"` by the Aztec macro +/// system and is accessible through the `self` variable. /// /// ## Usage in Contract Functions /// /// Once injected, you can use `self` to: /// - Access storage: `self.storage.balances.at(owner).read()` /// - Call contracts: `self.call(Token::at(address).transfer(recipient, amount))` -/// - Emit events: `self.emit(event).deliver_to(recipient, delivery_mode)` (private) or `self.emit(event)` (public) +/// - Emit events: `self.emit(event).deliver_to(recipient, delivery_mode)` /// - Get the contract address: `self.address` /// - Get the caller: `self.msg_sender()` /// - Access low-level Aztec.nr APIs through the context: `self.context` @@ -49,14 +40,14 @@ use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; /// /// ## Type Parameters /// -/// - `Context`: The execution context type - either `&mut PrivateContext`, `PublicContext`, or `UtilityContext` /// - `Storage`: The contract's storage struct (defined with [`storage`](crate::macros::storage::storage), or `()` if /// the contract has no storage /// - `CallSelf`: Macro-generated type for calling contract's own non-view functions /// - `EnqueueSelf`: Macro-generated type for enqueuing calls to the contract's own non-view functions /// - `CallSelfStatic`: Macro-generated type for calling contract's own view functions /// - `EnqueueSelfStatic`: Macro-generated type for enqueuing calls to the contract's own view functions -pub struct ContractSelf { +/// - `CallInternal`: Macro-generated type for calling internal functions +pub struct ContractSelfPrivate { /// The address of this contract pub address: AztecAddress, @@ -64,10 +55,8 @@ pub struct ContractSelf ContractSelf<&mut PrivateContext, Storage, CallSelf, EnqueueSelf, CallSelfStatic, EnqueueSelfStatic, CallInternal> { - /// Creates a new `ContractSelf` instance for a private function. +impl ContractSelfPrivate { + /// Creates a new `ContractSelfPrivate` instance for a private function. /// /// This constructor is called automatically by the macro system and should not be called directly. - pub fn new_private( + pub fn new( context: &mut PrivateContext, storage: Storage, call_self: CallSelf, @@ -335,8 +304,8 @@ impl ContractSelf { - /// Creates a new `ContractSelf` instance for a public function. - /// - /// This constructor is called automatically by the macro system and should not be called directly. - pub fn new_public( - context: PublicContext, - storage: Storage, - call_self: CallSelf, - call_self_static: CallSelfStatic, - internal: CallInternal, - ) -> Self { - Self { - context, - storage, - address: context.this_address(), - call_self, - enqueue_self: (), - call_self_static, - enqueue_self_static: (), - internal, - } - } - - /// The address of the contract address that made this function call. - /// - /// This is similar to Solidity's `msg.sender` value. - /// - /// ## Incognito Calls - /// - /// Contracts can call public functions from private ones hiding their identity (see - /// [`enqueue_incognito`](ContractSelf::enqueue_incognito)). This function reverts when executed in such a context. - /// - /// If you need to handle these cases, use [`PublicContext::maybe_msg_sender`]. - pub fn msg_sender(self: Self) -> AztecAddress { - self.context.maybe_msg_sender().unwrap() - } - - /// Emits an event publicly. - /// - /// Public events are emitted as plaintext and are therefore visible to everyone. This is is the same as Solidity - /// events on EVM chains. - /// - /// Unlike private events, they don't require delivery of an event message. - /// - /// # Example - /// ```noir - /// #[event] - /// struct Update { value: Field } - /// - /// #[external("public")] - /// fn publish_update(value: Field) { - /// self.emit(Update { value }); - /// } - /// ``` - /// - /// # Cost - /// - /// Public event emission is achieved by emitting public transaction logs. A total of `N+1` fields are emitted, - /// where `N` is the serialization length of the event. - pub fn emit(&mut self, event: Event) - where - Event: EventInterface + Serialize, - { - emit_event_in_public(self.context, event); - } - - /// Makes a public contract call. - /// - /// Will revert if the called function reverts or runs out of gas. - /// - /// # Arguments - /// * `call` - The object representing the public function to invoke. - /// - /// # Returns - /// * `T` - Whatever data the called function has returned. - /// - /// # Example - /// ```noir - /// self.call(Token::at(address).transfer_in_public(recipient, amount)); - /// ``` - /// - pub unconstrained fn call(self, call: PublicCall) -> T - where - T: Deserialize, - { - call.call(self.context) - } - - /// Makes a public read-only contract call. - /// - /// This is similar to Solidity's `staticcall`. The called function cannot modify state or emit events. Any nested - /// calls are constrained to also be static calls. - /// - /// Will revert if the called function reverts or runs out of gas. - /// - /// # Arguments - /// * `call` - The object representing the read-only public function to invoke. - /// - /// # Returns - /// * `T` - Whatever data the called function has returned. - /// - /// # Example - /// ```noir - /// self.view(Token::at(address).balance_of_public(recipient)); - /// ``` - /// - pub unconstrained fn view(self, call: PublicStaticCall) -> T - where - T: Deserialize, - { - call.view(self.context) - } -} - -// Implementation for `ContractSelf` in utility execution contexts. -// -// This implementation is used when an external or internal contract function is marked with "utility". -impl ContractSelf { - /// Creates a new `ContractSelf` instance for a utility function. - /// - /// This constructor is called automatically by the macro system and should not be called directly. - pub fn new_utility(context: UtilityContext, storage: Storage) -> Self { - Self { - context, - storage, - address: context.this_address(), - call_self: (), - enqueue_self: (), - call_self_static: (), - enqueue_self_static: (), - internal: (), - } - } -} diff --git a/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_public.nr b/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_public.nr new file mode 100644 index 000000000000..ab5026bffaa9 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_public.nr @@ -0,0 +1,174 @@ +//! The `self` contract value for public execution contexts. + +use crate::{ + context::{calls::{PublicCall, PublicStaticCall}, PublicContext}, + event::{event_emission::emit_event_in_public, event_interface::EventInterface}, +}; +use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}}; + +/// Core interface for interacting with aztec-nr contract features in public execution contexts. +/// +/// This struct is automatically injected into every [`external`](crate::macros::functions::external) and +/// [`internal`](crate::macros::functions::internal) contract function marked with `"public"` by the Aztec macro +/// system and is accessible through the `self` variable. +/// +/// ## Type Parameters +/// +/// - `Storage`: The contract's storage struct (defined with [`storage`](crate::macros::storage::storage), or `()` if +/// the contract has no storage +/// - `CallSelf`: Macro-generated type for calling contract's own non-view functions +/// - `CallSelfStatic`: Macro-generated type for calling contract's own view functions +/// - `CallInternal`: Macro-generated type for calling internal functions +pub struct ContractSelfPublic { + /// The address of this contract + pub address: AztecAddress, + + /// The contract's storage instance, representing the struct to which the + /// [`storage`](crate::macros::storage::storage) macro was applied in your contract. If the contract has no + /// storage, the type of this will be `()`. + /// + /// This storage instance is specialized for the current execution context (public) and + /// provides access to the contract's state variables. + /// + /// ## Developer Note + /// + /// If you've arrived here while trying to access your contract's storage while the `Storage` generic type is set + /// to unit type `()`, it means you haven't yet defined a Storage struct using the + /// [`storage`](crate::macros::storage::storage) macro in your contract. For guidance on setting this up, please + /// refer to our docs: https://docs.aztec.network/developers/docs/guides/smart_contracts/storage + pub storage: Storage, + + /// The public execution context. + pub context: PublicContext, + + /// Provides type-safe methods for calling this contract's own non-view functions. + /// + /// Example API: + /// ```noir + /// self.call_self.some_public_function(args) + /// ``` + pub call_self: CallSelf, + + /// Provides type-safe methods for calling this contract's own view functions. + /// + /// Example API: + /// ```noir + /// self.call_self_static.some_view_function(args) + /// ``` + pub call_self_static: CallSelfStatic, + + /// Provides type-safe methods for calling internal functions. + /// + /// Example API: + /// ```noir + /// self.internal.some_internal_function(args) + /// ``` + pub internal: CallInternal, +} + +impl ContractSelfPublic { + /// Creates a new `ContractSelfPublic` instance for a public function. + /// + /// This constructor is called automatically by the macro system and should not be called directly. + pub fn new( + context: PublicContext, + storage: Storage, + call_self: CallSelf, + call_self_static: CallSelfStatic, + internal: CallInternal, + ) -> Self { + Self { context, storage, address: context.this_address(), call_self, call_self_static, internal } + } + + /// The address of the contract address that made this function call. + /// + /// This is similar to Solidity's `msg.sender` value. + /// + /// ## Incognito Calls + /// + /// Contracts can call public functions from private ones hiding their identity (see + /// + /// [`ContractSelfPrivate::enqueue_incognito`](crate::contract_self::ContractSelfPrivate::enqueue_incognito)). + /// This function reverts when executed in such a context. + /// + /// If you need to handle these cases, use [`PublicContext::maybe_msg_sender`]. + pub fn msg_sender(self: Self) -> AztecAddress { + self.context.maybe_msg_sender().unwrap() + } + + /// Emits an event publicly. + /// + /// Public events are emitted as plaintext and are therefore visible to everyone. This is is the same as Solidity + /// events on EVM chains. + /// + /// Unlike private events, they don't require delivery of an event message. + /// + /// # Example + /// ```noir + /// #[event] + /// struct Update { value: Field } + /// + /// #[external("public")] + /// fn publish_update(value: Field) { + /// self.emit(Update { value }); + /// } + /// ``` + /// + /// # Cost + /// + /// Public event emission is achieved by emitting public transaction logs. A total of `N+1` fields are emitted, + /// where `N` is the serialization length of the event. + pub fn emit(&mut self, event: Event) + where + Event: EventInterface + Serialize, + { + emit_event_in_public(self.context, event); + } + + /// Makes a public contract call. + /// + /// Will revert if the called function reverts or runs out of gas. + /// + /// # Arguments + /// * `call` - The object representing the public function to invoke. + /// + /// # Returns + /// * `T` - Whatever data the called function has returned. + /// + /// # Example + /// ```noir + /// self.call(Token::at(address).transfer_in_public(recipient, amount)); + /// ``` + /// + pub unconstrained fn call(self, call: PublicCall) -> T + where + T: Deserialize, + { + call.call(self.context) + } + + /// Makes a public read-only contract call. + /// + /// This is similar to Solidity's `staticcall`. The called function cannot modify state or emit events. Any nested + /// calls are constrained to also be static calls. + /// + /// Will revert if the called function reverts or runs out of gas. + /// + /// # Arguments + /// * `call` - The object representing the read-only public function to invoke. + /// + /// # Returns + /// * `T` - Whatever data the called function has returned. + /// + /// # Example + /// ```noir + /// self.view(Token::at(address).balance_of_public(recipient)); + /// ``` + /// + pub unconstrained fn view(self, call: PublicStaticCall) -> T + where + T: Deserialize, + { + call.view(self.context) + } +} diff --git a/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_utility.nr b/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_utility.nr new file mode 100644 index 000000000000..39cc1da0d3ac --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/contract_self/contract_self_utility.nr @@ -0,0 +1,45 @@ +//! The `self` contract value for utility execution contexts. + +use crate::context::UtilityContext; +use crate::protocol::address::AztecAddress; + +/// Core interface for interacting with aztec-nr contract features in utility execution contexts. +/// +/// This struct is automatically injected into every [`external`](crate::macros::functions::external) contract function +/// marked with `"utility"` by the Aztec macro system and is accessible through the `self` variable. +/// +/// ## Type Parameters +/// +/// - `Storage`: The contract's storage struct (defined with [`storage`](crate::macros::storage::storage), or `()` if +/// the contract has no storage +pub struct ContractSelfUtility { + /// The address of this contract + pub address: AztecAddress, + + /// The contract's storage instance, representing the struct to which the + /// [`storage`](crate::macros::storage::storage) macro was applied in your contract. If the contract has no + /// storage, the type of this will be `()`. + /// + /// This storage instance is specialized for the current execution context (utility) and + /// provides access to the contract's state variables. + /// + /// ## Developer Note + /// + /// If you've arrived here while trying to access your contract's storage while the `Storage` generic type is set + /// to unit type `()`, it means you haven't yet defined a Storage struct using the + /// [`storage`](crate::macros::storage::storage) macro in your contract. For guidance on setting this up, please + /// refer to our docs: https://docs.aztec.network/developers/docs/guides/smart_contracts/storage + pub storage: Storage, + + /// The utility execution context. + pub context: UtilityContext, +} + +impl ContractSelfUtility { + /// Creates a new `ContractSelfUtility` instance for a utility function. + /// + /// This constructor is called automatically by the macro system and should not be called directly. + pub fn new(context: UtilityContext, storage: Storage) -> Self { + Self { context, storage, address: context.this_address() } + } +} diff --git a/noir-projects/aztec-nr/aztec/src/contract_self/mod.nr b/noir-projects/aztec-nr/aztec/src/contract_self/mod.nr new file mode 100644 index 000000000000..e97c764a38e8 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/contract_self/mod.nr @@ -0,0 +1,7 @@ +pub mod contract_self_private; +pub mod contract_self_public; +pub mod contract_self_utility; + +pub use contract_self_private::ContractSelfPrivate; +pub use contract_self_public::ContractSelfPublic; +pub use contract_self_utility::ContractSelfUtility; diff --git a/noir-projects/aztec-nr/aztec/src/event/event_emission.nr b/noir-projects/aztec-nr/aztec/src/event/event_emission.nr index 89141b717278..845c2f366f6c 100644 --- a/noir-projects/aztec-nr/aztec/src/event/event_emission.nr +++ b/noir-projects/aztec-nr/aztec/src/event/event_emission.nr @@ -11,7 +11,7 @@ pub struct NewEvent { pub(crate) randomness: Field, } -/// Equivalent to `self.emit(event)`: see [`crate::contract_self::ContractSelf::emit`]. +/// Equivalent to `self.emit(event)`: see [`crate::contract_self::ContractSelfPrivate::emit`]. pub fn emit_event_in_private(context: &mut PrivateContext, event: Event) -> EventMessage where Event: EventInterface + Serialize, @@ -34,7 +34,7 @@ where EventMessage::new(NewEvent { event, randomness }, context) } -/// Equivalent to `self.emit(event)`: see [`crate::contract_self::ContractSelf::emit`]. +/// Equivalent to `self.emit(event)`: see [`crate::contract_self::ContractSelfPublic::emit`]. pub fn emit_event_in_public(context: PublicContext, event: Event) where Event: EventInterface + Serialize, diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index d0118d08a9b7..d0cce41274d4 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -28,8 +28,7 @@ //! [`TestEnvironment`](crate::test::helpers::test_environment::TestEnvironment) and [mocks](crate::test::mocks). pub mod context; -mod contract_self; -pub use contract_self::ContractSelf; +pub mod contract_self; pub mod publish_contract_instance; pub mod hash; pub mod history; diff --git a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr index dfcd4eec59d5..75ab13d794ee 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr @@ -220,7 +220,7 @@ pub comptime fn only_self(f: FunctionDefinition) { /// ## Guarantees /// /// [`view`] functions can *only* be called in a static execution context, which is typically achieved by calling the -/// [`crate::contract_self::ContractSelf::view`] method on `self`. +/// [`crate::contract_self::ContractSelfPublic::view`] method on `self`. /// /// No compile time checks are performed on whether a function can be made [`view`]. If a function marked as view /// attempts to modify state, that will result in *runtime* failures. diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr index 035d0106284c..88c0f8590d4d 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/private.nr @@ -34,9 +34,9 @@ pub(crate) comptime fn generate_private_external(f: FunctionDefinition) -> Quote let storage = Storage::init(&mut context); } } else { - // Contract does not have Storage defined, so we set storage to the unit type `()`. ContractSelf requires a - // storage struct in its constructor. Using an Option type would lead to worse developer experience and higher - // constraint counts so we use the unit type `()` instead. + // Contract does not have Storage defined, so we set storage to the unit type `()`. ContractSelfPrivate + // requires a storage struct in its constructor. Using an Option type would lead to worse developer experience + // and higher constraint counts so we use the unit type `()` instead. quote { let storage = (); } @@ -55,7 +55,7 @@ pub(crate) comptime fn generate_private_external(f: FunctionDefinition) -> Quote let call_self_static: CallSelfStatic<&mut aztec::context::PrivateContext> = CallSelfStatic { address: self_address, context: &mut context }; let enqueue_self_static: EnqueueSelfStatic<&mut aztec::context::PrivateContext> = EnqueueSelfStatic { address: self_address, context: &mut context }; let internal: CallInternal<&mut aztec::context::PrivateContext> = CallInternal { context: &mut context }; - aztec::ContractSelf::new_private(&mut context, storage, call_self, enqueue_self, call_self_static, enqueue_self_static, internal) + aztec::contract_self::ContractSelfPrivate::new(&mut context, storage, call_self, enqueue_self, call_self_static, enqueue_self_static, internal) }; }; diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr index 8a575ab661b8..d481fd0a9666 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/public.nr @@ -33,9 +33,9 @@ pub(crate) comptime fn generate_public_external(f: FunctionDefinition) -> Quoted let storage = Storage::init(context); } } else { - // Contract does not have Storage defined, so we set storage to the unit type `()`. ContractSelf requires a - // storage struct in its constructor. Using an Option type would lead to worse developer experience and higher - // constraint counts so we use the unit type `()` instead. + // Contract does not have Storage defined, so we set storage to the unit type `()`. ContractSelfPublic requires + // a storage struct in its constructor. Using an Option type would lead to worse developer experience and + // higher constraint counts so we use the unit type `()` instead. quote { let storage = (); } @@ -55,7 +55,7 @@ pub(crate) comptime fn generate_public_external(f: FunctionDefinition) -> Quoted let call_self: CallSelf = CallSelf { address: self_address, context }; let call_self_static: CallSelfStatic = CallSelfStatic { address: self_address, context }; let internal: CallInternal = CallInternal { context }; - aztec::ContractSelf::new_public(context, storage, call_self, call_self_static, internal) + aztec::contract_self::ContractSelfPublic::new(context, storage, call_self, call_self_static, internal) }; }; diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr index f7f4da0a6904..f50bbc1249d7 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/external/utility.nr @@ -7,7 +7,8 @@ pub(crate) comptime fn generate_utility_external(f: FunctionDefinition) -> Quote let storage = Storage::init(context); } } else { - // Contract does not have Storage defined, so we set storage to the unit type `()`. ContractSelf requires a + // Contract does not have Storage defined, so we set storage to the unit type `()`. ContractSelfUtility + // requires a // storage struct in its constructor. Using an Option type would lead to worse developer experience and higher // constraint counts so we use the unit type `()` instead. quote { @@ -21,7 +22,7 @@ pub(crate) comptime fn generate_utility_external(f: FunctionDefinition) -> Quote let mut self = { let context = aztec::context::UtilityContext::new(); $storage_init - aztec::ContractSelf::new_utility(context, storage) + aztec::contract_self::ContractSelfUtility::new(context, storage) }; }; diff --git a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/internal.nr b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/internal.nr index 9aabae9e6a6c..443daa28f564 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/internal.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/internals_functions_generation/internal.nr @@ -42,7 +42,7 @@ pub(crate) comptime fn generate_private_internal(f: FunctionDefinition) -> Quote let call_self_static: CallSelfStatic<&mut aztec::context::PrivateContext> = CallSelfStatic { address: self_address, context }; let enqueue_self_static: EnqueueSelfStatic<&mut aztec::context::PrivateContext> = EnqueueSelfStatic { address: self_address, context }; let internal: CallInternal<&mut aztec::context::PrivateContext> = CallInternal { context }; - aztec::ContractSelf::new_private(context, storage, call_self, enqueue_self, call_self_static, enqueue_self_static, internal) + aztec::contract_self::ContractSelfPrivate::new(context, storage, call_self, enqueue_self, call_self_static, enqueue_self_static, internal) }; $body @@ -92,7 +92,7 @@ pub(crate) comptime fn generate_public_internal(f: FunctionDefinition) -> Quoted let call_self: CallSelf = CallSelf { address: self_address, context }; let call_self_static: CallSelfStatic = CallSelfStatic { address: self_address, context }; let internal: CallInternal = CallInternal { context }; - aztec::ContractSelf::new_public(context, storage, call_self, call_self_static, internal) + aztec::contract_self::ContractSelfPublic::new(context, storage, call_self, call_self_static, internal) }; $body diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr index 17ec576abaa6..e761110cde3f 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr @@ -29,7 +29,8 @@ mod test; /// /// Unlike [`PublicMutable`](crate::state_vars::PublicMutable) it is **also** possible to read a `PublicImmutable` from /// a private contract function, though it is not possible to initialize one. A common pattern is to have these -/// functions [enqueue a public self calls](crate::contract_self::ContractSelf::enqueue_self) in which the +/// functions [enqueue a public self calls](crate::contract_self::ContractSelfPrivate::enqueue) +/// in which the /// initialization operation is performed. /// /// For a mutable (with restrictions) variant which also can be read from private functions see diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr index 7d21664b280e..92d61704fa49 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr @@ -15,7 +15,8 @@ use crate::state_vars::StateVariable; /// A value stored in a `PublicMutable` can be read and written from public contract functions. /// /// It is not possible to read or write a `PublicMutable` from private contract functions. A common pattern is to have -/// these functions [enqueue a public self calls](crate::contract_self::ContractSelf::enqueue_self) in which the +/// these functions [enqueue a public self +/// calls](crate::contract_self::ContractSelfPrivate::enqueue) in which the /// required operation is performed. /// /// For an immutable variant which can be read from private functions, see diff --git a/noir-projects/noir-contracts/contracts/protocol/auth_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/protocol/auth_registry_contract/src/main.nr index 1e3117f31c7d..8b1af31ac0b0 100644 --- a/noir-projects/noir-contracts/contracts/protocol/auth_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/protocol/auth_registry_contract/src/main.nr @@ -105,12 +105,12 @@ pub contract AuthRegistry { self.storage.reject_all.at(on_behalf_of).read() } - /// Returns whether a specific `message_hash` is currently approved for `on_behalf_of`. - /// Does NOT check the `reject_all` flag - also check `is_reject_all` for a complete picture. + /// Returns whether a specific `message_hash` is currently consumable for `on_behalf_of`. #[external("public")] #[view] fn is_consumable(on_behalf_of: AztecAddress, message_hash: Field) -> bool { - self.storage.approved_actions.at(on_behalf_of).at(message_hash).read() + !self.storage.reject_all.at(on_behalf_of).read() + & self.storage.approved_actions.at(on_behalf_of).at(message_hash).read() } /// Utility version of `is_consumable` @@ -119,6 +119,7 @@ pub contract AuthRegistry { on_behalf_of: AztecAddress, message_hash: Field, ) -> bool { - self.storage.approved_actions.at(on_behalf_of).at(message_hash).read() + !self.storage.reject_all.at(on_behalf_of).read() + & self.storage.approved_actions.at(on_behalf_of).at(message_hash).read() } } diff --git a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr index 7c2bd5f5fb0b..821aeaf1c1c0 100644 --- a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr @@ -299,31 +299,35 @@ pub contract PendingNoteHashes { //} #[external("private")] - fn test_recursively_create_notes(owner: AztecAddress, how_many_recursions: u64) { + fn test_recursively_create_notes(recipients: [AztecAddress; 10], how_many_recursions: u64) { let initial_offset: u64 = 0; - self.internal.create_max_notes(owner, initial_offset); + self.internal.create_max_notes(recipients, initial_offset); let max_notes = self.internal.max_notes_per_call() as u64; - self.call_self.recursively_destroy_and_create_notes(owner, how_many_recursions, max_notes); + self.call_self.recursively_destroy_and_create_notes( + recipients, + how_many_recursions, + max_notes, + ); } #[external("private")] fn recursively_destroy_and_create_notes( - owner: AztecAddress, + recipients: [AztecAddress; 10], executions_left: u64, current_offset: u64, ) { assert(executions_left > 0); - self.internal.destroy_max_notes(owner); - self.internal.create_max_notes(owner, current_offset); + self.internal.destroy_max_notes(recipients); + self.internal.create_max_notes(recipients, current_offset); let executions_left = executions_left - 1; if executions_left > 0 { let max_notes = self.internal.max_notes_per_call() as u64; self.call_self.recursively_destroy_and_create_notes( - owner, + recipients, executions_left, current_offset + max_notes, ); @@ -331,21 +335,30 @@ pub contract PendingNoteHashes { } #[internal("private")] - fn create_max_notes(owner: AztecAddress, offset: u64) { - let owner_balance = self.storage.balances.at(owner); + fn create_max_notes(recipients: [AztecAddress; 10], offset: u64) { + // Distribute notes across recipients using global offset to ensure + // no recipient receives more than 10 notes (UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN) for i in 0..self.internal.max_notes_per_call() { - let note = FieldNote { value: (offset + i as u64) as Field }; - // Skip deliver(): notes are created and nullified in the same tx (kernel squashing), - // so tagged log delivery is unnecessary. Delivering would also exceed - // UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN for the sender-recipient pair. - let _ = owner_balance.insert(note); + let global_index = offset + i as u64; + let recipient_index = (global_index % 10) as u32; + let recipient = recipients[recipient_index]; + let recipient_balance = self.storage.balances.at(recipient); + + let note = FieldNote { value: i as Field }; + recipient_balance.insert(note).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); } } #[internal("private")] - fn destroy_max_notes(owner: AztecAddress) { - let owner_balance = self.storage.balances.at(owner); - let _ = owner_balance.pop_notes(NoteGetterOptions::new()); + fn destroy_max_notes(recipients: [AztecAddress; 10]) { + // Pop notes from all recipients + for i in 0..10 { + let recipient = recipients[i]; + let recipient_balance = self.storage.balances.at(recipient); + // Note that we're relying on PXE actually returning the notes, we're not constraining that any specific + // number of notes are deleted. + let _ = recipient_balance.pop_notes(NoteGetterOptions::new()); + } } #[internal("private")] diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 0706015744ca..88bb1404b59e 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -1236,7 +1236,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config); // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx - const [processedTxs, failedTxs, _usedTxs, returns] = await processor.process([tx]); + const [processedTxs, failedTxs, _usedTxs, returns, _blobFields, debugLogs] = await processor.process([tx]); // REFACTOR: Consider returning the error rather than throwing if (failedTxs.length) { this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash }); @@ -1250,6 +1250,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable { processedTx.txEffect, returns, processedTx.gasUsed, + debugLogs, ); } finally { await merkleTreeFork.close(); diff --git a/yarn-project/aztec.js/src/contract/deploy_method.ts b/yarn-project/aztec.js/src/contract/deploy_method.ts index 5c4d9544aa2e..36fcf609fc92 100644 --- a/yarn-project/aztec.js/src/contract/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract/deploy_method.ts @@ -82,7 +82,7 @@ export type DeployOptionsWithoutWait = Omit & * is mutually exclusive with "deployer" */ universalDeploy?: boolean; -} & Pick; +} & Pick; /** * Extends the deployment options with the required parameters to send the transaction. diff --git a/yarn-project/aztec.js/src/contract/interaction_options.ts b/yarn-project/aztec.js/src/contract/interaction_options.ts index 2651c9f5f49d..b3655f825fa2 100644 --- a/yarn-project/aztec.js/src/contract/interaction_options.ts +++ b/yarn-project/aztec.js/src/contract/interaction_options.ts @@ -82,6 +82,13 @@ export type SendInteractionOptionsWithoutWait = RequestInteractionOptions & { from: AztecAddress; /** The fee options for the transaction. */ fee?: InteractionFeeOptions; + /** + * Additional addresses whose private state and keys should be accessible during execution, + * beyond the sender's. Required when the transaction needs to access private state or keys + * belonging to an address other than `from`, e.g. withdrawing from an escrow that holds + * its own private notes. + */ + additionalScopes?: AztecAddress[]; }; /** diff --git a/yarn-project/aztec.js/src/wallet/capabilities.ts b/yarn-project/aztec.js/src/wallet/capabilities.ts index 54a6a3c2b42d..fe015195101d 100644 --- a/yarn-project/aztec.js/src/wallet/capabilities.ts +++ b/yarn-project/aztec.js/src/wallet/capabilities.ts @@ -32,6 +32,15 @@ export interface ContractFunctionPattern { /** Function name or '*' for any function */ function: string; + + /** + * Additional addresses whose private state and keys are accessible + * when calling this function, beyond the sender's. + * - undefined: No additional scopes allowed + * - AztecAddress[]: Only these specific addresses allowed as additional scopes + * - '*': All known address allowed as an additional scope + */ + additionalScopes?: AztecAddress[] | '*'; } /** diff --git a/yarn-project/aztec.js/src/wallet/wallet.test.ts b/yarn-project/aztec.js/src/wallet/wallet.test.ts index bcee66440e18..298f279b07b3 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.test.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.test.ts @@ -226,6 +226,8 @@ describe('WalletSchema', () => { }); it('requestCapabilities', async () => { + const someAddress = await AztecAddress.random(); + const anotherAddress = await AztecAddress.random(); const manifest: AppCapabilities = { version: '1.0', metadata: { @@ -239,6 +241,14 @@ describe('WalletSchema', () => { canGet: true, canCreateAuthWit: true, }, + { + type: 'transaction', + scope: [ + { contract: someAddress, function: 'withdraw', additionalScopes: [anotherAddress] }, + { contract: someAddress, function: 'transfer' }, + { contract: someAddress, function: 'deposit', additionalScopes: '*' }, + ], + }, ], }; const result = await context.client.requestCapabilities(manifest); diff --git a/yarn-project/aztec.js/src/wallet/wallet.ts b/yarn-project/aztec.js/src/wallet/wallet.ts index 9918542d297e..f4c3c296ee5b 100644 --- a/yarn-project/aztec.js/src/wallet/wallet.ts +++ b/yarn-project/aztec.js/src/wallet/wallet.ts @@ -303,6 +303,7 @@ export const SendOptionsSchema = z.object({ capsules: optional(z.array(Capsule.schema)), fee: optional(GasSettingsOptionSchema), wait: optional(z.union([z.literal(NO_WAIT), WaitOptsSchema])), + additionalScopes: optional(z.array(schemas.AztecAddress)), }); export const SimulateOptionsSchema = z.object({ @@ -313,6 +314,7 @@ export const SimulateOptionsSchema = z.object({ skipTxValidation: optional(z.boolean()), skipFeeEnforcement: optional(z.boolean()), includeMetadata: optional(z.boolean()), + additionalScopes: optional(z.array(schemas.AztecAddress)), }); export const ProfileOptionsSchema = SimulateOptionsSchema.extend({ @@ -379,6 +381,7 @@ export const ContractClassMetadataSchema = z.object({ export const ContractFunctionPatternSchema = z.object({ contract: z.union([schemas.AztecAddress, z.literal('*')]), function: z.union([z.string(), z.literal('*')]), + additionalScopes: optional(z.union([z.array(schemas.AztecAddress), z.literal('*')])), }); export const AccountsCapabilitySchema = z.object({ diff --git a/yarn-project/aztec/bootstrap.sh b/yarn-project/aztec/bootstrap.sh new file mode 100755 index 000000000000..337b7e40d9c3 --- /dev/null +++ b/yarn-project/aztec/bootstrap.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +source $(git rev-parse --show-toplevel)/ci3/source_bootstrap + +repo_root=$(git rev-parse --show-toplevel) +export NARGO=${NARGO:-$repo_root/noir/noir-repo/target/release/nargo} +export BB=${BB:-$repo_root/barretenberg/cpp/build/bin/bb} + +hash=$(../bootstrap.sh hash) + +function test_cmds { + echo "$hash:ISOLATE=1:NAME=aztec/src/cli/cmds/compile.test.ts NARGO=$NARGO BB=$BB yarn-project/scripts/run_test.sh aztec/src/cli/cmds/compile.test.ts" +} + +case "$cmd" in + "") + ;; + *) + default_cmd_handler "$@" + ;; +esac diff --git a/yarn-project/aztec/scripts/aztec.sh b/yarn-project/aztec/scripts/aztec.sh index 0007c5c8560c..6de3f16ac854 100755 --- a/yarn-project/aztec/scripts/aztec.sh +++ b/yarn-project/aztec/scripts/aztec.sh @@ -53,7 +53,7 @@ case $cmd in aztec start "$@" ;; - compile|new|init|flamegraph) + new|init|flamegraph) $script_dir/${cmd}.sh "$@" ;; *) diff --git a/yarn-project/aztec/scripts/compile.sh b/yarn-project/aztec/scripts/compile.sh deleted file mode 100755 index 7bec1e29d17f..000000000000 --- a/yarn-project/aztec/scripts/compile.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -NARGO=${NARGO:-nargo} -BB=${BB:-bb} - -# If help is requested, show Aztec-specific info then run nargo compile help and then exit in order to not trigger -# transpilation -for arg in "$@"; do - if [ "$arg" == "--help" ] || [ "$arg" == "-h" ]; then - cat << 'EOF' -Aztec Compile - Compile Aztec Noir contracts - -This command compiles Aztec Noir contracts using nargo and then automatically -postprocesses them to generate Aztec specific artifacts including: -- Transpiled contract artifacts -- Verification keys - -The compiled contracts will be placed in the target/ directory by default. - ---- -Underlying nargo compile options: - -EOF - nargo compile --help - exit 0 - fi -done - -# Run nargo compile. -$NARGO compile "$@" - -echo "Postprocessing contract..." -$BB aztec_process - -# Strip internal prefixes from all compiled contract JSONs in target directory -# TODO: This should be part of bb aztec_process! -for json in target/*.json; do - temp_file="${json}.tmp" - jq '.functions |= map(.name |= sub("^__aztec_nr_internals__"; ""))' "$json" > "$temp_file" - mv "$temp_file" "$json" -done - -echo "Compilation complete!" diff --git a/yarn-project/aztec/src/bin/index.ts b/yarn-project/aztec/src/bin/index.ts index d06b298d9add..90a915778b17 100644 --- a/yarn-project/aztec/src/bin/index.ts +++ b/yarn-project/aztec/src/bin/index.ts @@ -14,6 +14,7 @@ import { createConsoleLogger, createLogger } from '@aztec/foundation/log'; import { Command } from 'commander'; +import { injectCompileCommand } from '../cli/cmds/compile.js'; import { injectMigrateCommand } from '../cli/cmds/migrate_ha_db.js'; import { injectAztecCommands } from '../cli/index.js'; import { getCliVersion } from '../cli/release_version.js'; @@ -47,7 +48,7 @@ async function main() { const cliVersion = getCliVersion(); let program = new Command('aztec'); - program.description('Aztec command line interface').version(cliVersion); + program.description('Aztec command line interface').version(cliVersion).enablePositionalOptions(); program = injectAztecCommands(program, userLog, debugLogger); program = injectBuilderCommands(program); program = injectContractCommands(program, userLog, debugLogger); @@ -56,6 +57,7 @@ async function main() { program = injectAztecNodeCommands(program, userLog, debugLogger); program = injectMiscCommands(program, userLog); program = injectValidatorKeysCommands(program, userLog); + program = injectCompileCommand(program, userLog); program = injectMigrateCommand(program, userLog); await program.parseAsync(process.argv); diff --git a/yarn-project/aztec/src/cli/cli.ts b/yarn-project/aztec/src/cli/cli.ts index f086b852da31..1c79cf24f294 100644 --- a/yarn-project/aztec/src/cli/cli.ts +++ b/yarn-project/aztec/src/cli/cli.ts @@ -39,7 +39,6 @@ Additional commands: init [folder] [options] creates a new Aztec Noir project. new [options] creates a new Aztec Noir project in a new directory. - compile [options] compiles Aztec Noir contracts. test [options] starts a TXE and runs "nargo test" using it as the oracle resolver. `, ); diff --git a/yarn-project/aztec/src/cli/cmds/compile.test.ts b/yarn-project/aztec/src/cli/cmds/compile.test.ts new file mode 100644 index 000000000000..8a0af802b005 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/compile.test.ts @@ -0,0 +1,82 @@ +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { execFileSync } from 'child_process'; +import { existsSync, readFileSync, rmSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..'); +const CLI = join(PACKAGE_ROOT, 'dest/bin/index.js'); +const WORKSPACE = join(PACKAGE_ROOT, 'test/mixed-workspace'); +const TARGET = join(WORKSPACE, 'target'); +const CODEGEN_OUT = join(WORKSPACE, 'codegen-output'); + +// Compiles a mixed workspace containing both a contract and a plain circuit, +// then runs codegen. Validates that: +// - Contract artifacts have a functions array and are transpiled +// - Program (circuit) artifacts do not have functions and are not transpiled +// - Codegen produces TypeScript only for contracts, not circuits +describe('aztec compile integration', () => { + beforeAll(() => { + cleanupArtifacts(); + runCompile(); + runCodegen(); + }, 120_000); + + afterAll(() => { + cleanupArtifacts(); + }); + + it('contract artifact has functions array', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_contract-SimpleContract.json'), 'utf-8')); + expect(Array.isArray(artifact.functions)).toBe(true); + expect(artifact.functions.length).toBeGreaterThan(0); + }); + + it('program artifact does not have functions', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_circuit.json'), 'utf-8')); + expect(artifact.functions).toBeUndefined(); + }); + + it('contract artifact was transpiled', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_contract-SimpleContract.json'), 'utf-8')); + expect(artifact.transpiled).toBe(true); + }); + + it('program artifact was not transpiled', () => { + const artifact = JSON.parse(readFileSync(join(TARGET, 'simple_circuit.json'), 'utf-8')); + expect(artifact.transpiled).toBeFalsy(); + }); + + it('codegen produced TypeScript for contract', () => { + expect(existsSync(join(CODEGEN_OUT, 'SimpleContract.ts'))).toBe(true); + }); + + it('codegen did not produce TypeScript for circuit', () => { + expect(existsSync(join(CODEGEN_OUT, 'SimpleCircuit.ts'))).toBe(false); + }); +}); + +function cleanupArtifacts() { + rmSync(TARGET, { recursive: true, force: true }); + rmSync(CODEGEN_OUT, { recursive: true, force: true }); + rmSync(join(WORKSPACE, 'codegenCache.json'), { force: true }); +} + +function runCompile() { + try { + execFileSync('node', [CLI, 'compile'], { cwd: WORKSPACE, stdio: 'pipe' }); + } catch (e: any) { + throw new Error(`compile failed:\n${e.stderr?.toString() ?? e.message}`); + } +} + +function runCodegen() { + try { + execFileSync('node', [CLI, 'codegen', 'target', '-o', 'codegen-output', '-f'], { + cwd: WORKSPACE, + stdio: 'pipe', + }); + } catch (e: any) { + throw new Error(`codegen failed:\n${e.stderr?.toString() ?? e.message}`); + } +} diff --git a/yarn-project/aztec/src/cli/cmds/compile.ts b/yarn-project/aztec/src/cli/cmds/compile.ts new file mode 100644 index 000000000000..9fb8aba37f03 --- /dev/null +++ b/yarn-project/aztec/src/cli/cmds/compile.ts @@ -0,0 +1,107 @@ +import type { LogFn } from '@aztec/foundation/log'; + +import { execFileSync, spawn } from 'child_process'; +import type { Command } from 'commander'; +import { readFile, readdir, writeFile } from 'fs/promises'; +import { join } from 'path'; + +/** Spawns a command with inherited stdio and rejects on non-zero exit. */ +function run(cmd: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const child = spawn(cmd, args, { stdio: 'inherit' }); + child.on('error', reject); + child.on('close', code => { + if (code !== 0) { + reject(new Error(`${cmd} exited with code ${code}`)); + } else { + resolve(); + } + }); + }); +} + +/** Returns paths to contract artifacts in the target directory. + * Contract artifacts are identified by having a `functions` array in the JSON. + */ +async function collectContractArtifacts(): Promise { + let files: string[]; + try { + files = await readdir('target'); + } catch (err: any) { + if (err?.code === 'ENOENT') { + return []; + } + throw new Error(`Failed to read target directory: ${err.message}`); + } + + const artifacts: string[] = []; + for (const file of files) { + if (!file.endsWith('.json')) { + continue; + } + const filePath = join('target', file); + const content = JSON.parse(await readFile(filePath, 'utf-8')); + if (Array.isArray(content.functions)) { + artifacts.push(filePath); + } + } + return artifacts; +} + +/** Strips the `__aztec_nr_internals__` prefix from function names in contract artifacts. */ +async function stripInternalPrefixes(artifactPaths: string[]): Promise { + for (const path of artifactPaths) { + const artifact = JSON.parse(await readFile(path, 'utf-8')); + for (const fn of artifact.functions) { + if (typeof fn.name === 'string') { + fn.name = fn.name.replace(/^__aztec_nr_internals__/, ''); + } + } + await writeFile(path, JSON.stringify(artifact, null, 2) + '\n'); + } +} + +/** Compiles Aztec Noir contracts and postprocesses artifacts. */ +async function compileAztecContract(nargoArgs: string[], log: LogFn): Promise { + const nargo = process.env.NARGO ?? 'nargo'; + const bb = process.env.BB ?? 'bb'; + + await run(nargo, ['compile', ...nargoArgs]); + + const artifacts = await collectContractArtifacts(); + + if (artifacts.length > 0) { + log('Postprocessing contracts...'); + const bbArgs = artifacts.flatMap(a => ['-i', a]); + await run(bb, ['aztec_process', ...bbArgs]); + + // TODO: This should be part of bb aztec_process! + await stripInternalPrefixes(artifacts); + } + + log('Compilation complete!'); +} + +export function injectCompileCommand(program: Command, log: LogFn): Command { + program + .command('compile') + .argument('[nargo-args...]') + .passThroughOptions() + .allowUnknownOption() + .description( + 'Compile Aztec Noir contracts using nargo and postprocess them to generate transpiled artifacts and verification keys. All options are forwarded to nargo compile.', + ) + .addHelpText('after', () => { + // Show nargo's own compile options so users see all available flags in one place. + const nargo = process.env.NARGO ?? 'nargo'; + try { + const output = execFileSync(nargo, ['compile', '--help'], { encoding: 'utf-8' }); + return `\nUnderlying nargo compile options:\n\n${output}`; + } catch { + return '\n(Run "nargo compile --help" to see available nargo options)'; + } + }) + .action((nargoArgs: string[]) => compileAztecContract(nargoArgs, log)); + + return program; +} diff --git a/yarn-project/aztec/test/mixed-workspace/.gitignore b/yarn-project/aztec/test/mixed-workspace/.gitignore new file mode 100644 index 000000000000..8515795b7b4d --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/.gitignore @@ -0,0 +1,3 @@ +target/ +codegen-output/ +codegenCache.json diff --git a/yarn-project/aztec/test/mixed-workspace/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/Nargo.toml new file mode 100644 index 000000000000..2ef7da4e84a0 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/Nargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["simple_contract", "simple_circuit"] diff --git a/yarn-project/aztec/test/mixed-workspace/README.md b/yarn-project/aztec/test/mixed-workspace/README.md new file mode 100644 index 000000000000..83f385759655 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/README.md @@ -0,0 +1,26 @@ +# Mixed Workspace Test + +Regression test for `aztec compile` and `aztec codegen` in Nargo workspaces that +contain both Aztec contracts and plain Noir circuits. + +## Problem + +Both `aztec compile` and `aztec codegen` assumed every `.json` in `target/` is a +contract artifact. When a workspace also contains `type = "bin"` packages, the +resulting program artifacts lack `functions`/`name` fields, causing: + +- `bb aztec_process` to fail trying to transpile a program artifact +- The jq postprocessing step to fail on missing `.functions` +- `codegen` to crash calling `loadContractArtifact()` on a program artifact + +## What the test checks + +`yarn-project/aztec/src/cli/cmds/compile.test.ts` runs compile and codegen on +this workspace and verifies: + +1. Compilation succeeds without errors +2. Both artifacts exist in `target/` +3. The contract artifact was postprocessed (has `transpiled` field) +4. The program artifact was not modified (no `transpiled` field) +5. Codegen generates a TypeScript wrapper only for the contract +6. No TypeScript wrapper is generated for the program artifact diff --git a/yarn-project/aztec/test/mixed-workspace/simple_circuit/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/simple_circuit/Nargo.toml new file mode 100644 index 000000000000..a74e4a284148 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_circuit/Nargo.toml @@ -0,0 +1,7 @@ +[package] +name = "simple_circuit" +authors = [""] +compiler_version = ">=0.25.0" +type = "bin" + +[dependencies] diff --git a/yarn-project/aztec/test/mixed-workspace/simple_circuit/src/main.nr b/yarn-project/aztec/test/mixed-workspace/simple_circuit/src/main.nr new file mode 100644 index 000000000000..e149eb109fca --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_circuit/src/main.nr @@ -0,0 +1,3 @@ +fn main(x: Field) { + assert(x != 0); +} diff --git a/yarn-project/aztec/test/mixed-workspace/simple_contract/Nargo.toml b/yarn-project/aztec/test/mixed-workspace/simple_contract/Nargo.toml new file mode 100644 index 000000000000..681c51353ea5 --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "simple_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../../noir-projects/aztec-nr/aztec" } diff --git a/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr b/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr new file mode 100644 index 000000000000..43c4331608db --- /dev/null +++ b/yarn-project/aztec/test/mixed-workspace/simple_contract/src/main.nr @@ -0,0 +1,11 @@ +use aztec::macros::aztec; + +#[aztec] +pub contract SimpleContract { + use aztec::macros::functions::external; + + #[external("private")] + fn private_function() -> Field { + 0 + } +} diff --git a/yarn-project/bootstrap.sh b/yarn-project/bootstrap.sh index acacda5639f8..a7992576d9e5 100755 --- a/yarn-project/bootstrap.sh +++ b/yarn-project/bootstrap.sh @@ -221,6 +221,9 @@ function test_cmds { # Uses mocha for browser tests, so we have to treat it differently. echo "$hash:ISOLATE=1 cd yarn-project/kv-store && yarn test" + # Aztec CLI tests + aztec/bootstrap.sh test_cmds + if [[ "${TARGET_BRANCH:-}" =~ ^v[0-9]+$ ]]; then echo "$hash yarn-project/scripts/run_test.sh aztec/src/testnet_compatibility.test.ts" echo "$hash yarn-project/scripts/run_test.sh aztec/src/mainnet_compatibility.test.ts" diff --git a/yarn-project/bot/src/factory.ts b/yarn-project/bot/src/factory.ts index 77b474c559b0..31b05f540b8e 100644 --- a/yarn-project/bot/src/factory.ts +++ b/yarn-project/bot/src/factory.ts @@ -285,6 +285,8 @@ export class BotFactory { tokenInstance = await deploy.getInstance(deployOpts); token = PrivateTokenContract.at(tokenInstance.address, this.wallet); await this.wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey); + // The contract constructor initializes private storage vars that need the contract's own nullifier key. + deployOpts.additionalScopes = [tokenInstance.address]; } else { throw new Error(`Unsupported token contract type: ${this.config.contract}`); } @@ -479,8 +481,10 @@ export class BotFactory { return; } + // PrivateToken's mint accesses contract-level private storage vars (admin, total_supply). + const additionalScopes = isStandardToken ? undefined : [token.address]; await this.withNoMinTxsPerBlock(async () => { - const txHash = await new BatchCall(token.wallet, calls).send({ from: minter, wait: NO_WAIT }); + const txHash = await new BatchCall(token.wallet, calls).send({ from: minter, additionalScopes, wait: NO_WAIT }); this.log.info(`Sent token mint tx with hash ${txHash.toString()}`); return waitForTx(this.aztecNode, txHash, { timeout: this.config.txMinedWaitSeconds }); }); diff --git a/yarn-project/builder/src/contract-interface-gen/codegen.ts b/yarn-project/builder/src/contract-interface-gen/codegen.ts index 5321c5070d22..1137b81a6a6c 100644 --- a/yarn-project/builder/src/contract-interface-gen/codegen.ts +++ b/yarn-project/builder/src/contract-interface-gen/codegen.ts @@ -50,6 +50,12 @@ async function generateFromNoirAbi(outputPath: string, noirAbiPath: string, opts const file = await readFile(noirAbiPath, 'utf8'); const contract = JSON.parse(file); + + if (!Array.isArray(contract.functions)) { + console.log(`${fileName} is not a contract artifact. Skipping.`); + return; + } + const aztecAbi = loadContractArtifact(contract); await mkdir(outputPath, { recursive: true }); diff --git a/yarn-project/cli-wallet/src/cmds/create_account.ts b/yarn-project/cli-wallet/src/cmds/create_account.ts index 64b730b95aa1..771392adf469 100644 --- a/yarn-project/cli-wallet/src/cmds/create_account.ts +++ b/yarn-project/cli-wallet/src/cmds/create_account.ts @@ -79,6 +79,8 @@ export async function createAccount( skipInstancePublication: !publicDeploy, skipInitialization, from, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [address], fee: { paymentMethod, gasSettings }, }; diff --git a/yarn-project/cli-wallet/src/utils/wallet.ts b/yarn-project/cli-wallet/src/utils/wallet.ts index 2124b79197dd..a1493d0cc13c 100644 --- a/yarn-project/cli-wallet/src/utils/wallet.ts +++ b/yarn-project/cli-wallet/src/utils/wallet.ts @@ -87,7 +87,7 @@ export class CLIWallet extends BaseWallet { increasedFee: InteractionFeeOptions, ): Promise { const cancellationTxRequest = await this.createCancellationTxExecutionRequest(from, txNonce, increasedFee); - return await this.pxe.proveTx(cancellationTxRequest, this.scopesFor(from)); + return await this.pxe.proveTx(cancellationTxRequest, this.scopesFrom(from)); } override async getAccountFromAddress(address: AztecAddress) { diff --git a/yarn-project/end-to-end/src/composed/docs_examples.test.ts b/yarn-project/end-to-end/src/composed/docs_examples.test.ts index fe843da8ff56..70770dc40cad 100644 --- a/yarn-project/end-to-end/src/composed/docs_examples.test.ts +++ b/yarn-project/end-to-end/src/composed/docs_examples.test.ts @@ -30,7 +30,11 @@ describe('docs_examples', () => { const prefundedAccount = await wallet.createSchnorrAccount(accountData.secret, accountData.salt); const newAccountManager = await wallet.createSchnorrAccount(secretKey, Fr.random(), signingPrivateKey); const newAccountDeployMethod = await newAccountManager.getDeployMethod(); - await newAccountDeployMethod.send({ from: prefundedAccount.address }); + await newAccountDeployMethod.send({ + from: prefundedAccount.address, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [newAccountManager.address], + }); const newAccountAddress = newAccountManager.address; const defaultAccountAddress = prefundedAccount.address; diff --git a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts index 951c3e0152c4..4e6a5308e340 100644 --- a/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts +++ b/yarn-project/end-to-end/src/composed/e2e_local_network_example.test.ts @@ -145,7 +145,8 @@ describe('e2e_local_network_example', () => { return await Promise.all( accountManagers.map(async x => { const deployMethod = await x.getDeployMethod(); - await deployMethod.send({ from: fundedAccount }); + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + await deployMethod.send({ from: fundedAccount, additionalScopes: [x.address] }); return x; }), ); 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 e82734a14b15..5f892e4c2e11 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 @@ -43,7 +43,7 @@ describe('e2e_crowdfunding_and_claim', () => { let crowdfundingContract: CrowdfundingContract; let claimContract: ClaimContract; - let crowdfundingSecretKey; + let crowdfundingSecretKey: Fr; let crowdfundingPublicKeys: PublicKeys; let cheatCodes: CheatCodes; let deadline: number; // end of crowdfunding period @@ -94,7 +94,11 @@ describe('e2e_crowdfunding_and_claim', () => { ); const crowdfundingInstance = await crowdfundingDeployment.getInstance(); await wallet.registerContract(crowdfundingInstance, CrowdfundingContract.artifact, crowdfundingSecretKey); - crowdfundingContract = await crowdfundingDeployment.send({ from: operatorAddress }); + crowdfundingContract = await crowdfundingDeployment.send({ + from: operatorAddress, + // The contract constructor initializes private storage vars that need the contract's own nullifier key. + additionalScopes: [crowdfundingInstance.address], + }); logger.info(`Crowdfunding contract deployed at ${crowdfundingContract.address}`); claimContract = await ClaimContract.deploy(wallet, crowdfundingContract.address, rewardToken.address).send({ @@ -151,7 +155,10 @@ describe('e2e_crowdfunding_and_claim', () => { expect(balanceDNTBeforeWithdrawal).toEqual(0n); // 3) At last, we withdraw the raised funds from the crowdfunding contract to the operator's address - await crowdfundingContract.methods.withdraw(donationAmount).send({ from: operatorAddress }); + await crowdfundingContract.methods + .withdraw(donationAmount) + // Withdraw nullifies the contract's own token notes, which requires its nullifier key. + .send({ from: operatorAddress, additionalScopes: [crowdfundingContract.address] }); const balanceDNTAfterWithdrawal = await donationToken.methods .balance_of_private(operatorAddress) @@ -221,7 +228,13 @@ describe('e2e_crowdfunding_and_claim', () => { deadline, ); - otherCrowdfundingContract = await otherCrowdfundingDeployment.send({ from: operatorAddress }); + const otherCrowdfundingInstance = await otherCrowdfundingDeployment.getInstance(); + await wallet.registerContract(otherCrowdfundingInstance, CrowdfundingContract.artifact, crowdfundingSecretKey); + otherCrowdfundingContract = await otherCrowdfundingDeployment.send({ + from: operatorAddress, + // The contract constructor initializes private storage vars that need the contract's own nullifier key. + additionalScopes: [otherCrowdfundingInstance.address], + }); logger.info(`Crowdfunding contract deployed at ${otherCrowdfundingContract.address}`); } @@ -269,9 +282,12 @@ describe('e2e_crowdfunding_and_claim', () => { await crowdfundingContract.methods.donate(donationAmount).send({ from: donor2Address, authWitnesses: [witness] }); // The following should fail as msg_sender != operator - await expect(crowdfundingContract.methods.withdraw(donationAmount).send({ from: donor2Address })).rejects.toThrow( - 'Assertion failed: Not an operator', - ); + await expect( + crowdfundingContract.methods + .withdraw(donationAmount) + // Withdraw nullifies the contract's own token notes, which requires its nullifier key. + .send({ from: donor2Address, additionalScopes: [crowdfundingContract.address] }), + ).rejects.toThrow('Assertion failed: Not an operator'); }); it('cannot donate after a deadline', async () => { 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 d6efb8165374..a9fd2e501887 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 @@ -41,7 +41,8 @@ describe('e2e_escrow_contract', () => { const escrowDeployment = EscrowContract.deployWithPublicKeys(escrowPublicKeys, wallet, owner); const escrowInstance = await escrowDeployment.getInstance(); await wallet.registerContract(escrowInstance, EscrowContract.artifact, escrowSecretKey); - escrowContract = await escrowDeployment.send({ from: owner }); + // The contract constructor initializes private storage vars that need the contract's own nullifier key. + escrowContract = await escrowDeployment.send({ from: owner, additionalScopes: [escrowInstance.address] }); logger.info(`Escrow contract deployed at ${escrowContract.address}`); // Deploy Token contract and mint funds for the escrow contract @@ -60,7 +61,10 @@ describe('e2e_escrow_contract', () => { await expectTokenBalance(wallet, token, escrowContract.address, 100n, logger); logger.info(`Withdrawing funds from token contract to ${recipient}`); - await escrowContract.methods.withdraw(token.address, 30, recipient).send({ from: owner }); + await escrowContract.methods + .withdraw(token.address, 30, recipient) + // Withdraw nullifies the contract's own token notes, which requires its nullifier key. + .send({ from: owner, additionalScopes: [escrowContract.address] }); await expectTokenBalance(wallet, token, owner, 0n, logger); await expectTokenBalance(wallet, token, recipient, 30n, logger); @@ -69,7 +73,10 @@ describe('e2e_escrow_contract', () => { it('refuses to withdraw funds as a non-owner', async () => { await expect( - escrowContract.methods.withdraw(token.address, 30, recipient).simulate({ from: recipient }), + escrowContract.methods + .withdraw(token.address, 30, recipient) + // Withdraw nullifies the contract's own token notes, which requires its nullifier key. + .simulate({ from: recipient, additionalScopes: [escrowContract.address] }), ).rejects.toThrow(); }); @@ -84,7 +91,8 @@ describe('e2e_escrow_contract', () => { await new BatchCall(wallet, [ token.methods.transfer(recipient, 10), escrowContract.methods.withdraw(token.address, 20, recipient), - ]).send({ from: owner }); + // Withdraw nullifies the contract's own token notes, which requires its nullifier key. + ]).send({ from: owner, additionalScopes: [escrowContract.address] }); await expectTokenBalance(wallet, token, recipient, 30n, logger); }); }); diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index b93a429b2265..a62639ebf564 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -89,7 +89,12 @@ describe('e2e_fees account_init', () => { const [bobsInitialGas] = await t.getGasBalanceFn(bobsAddress); expect(bobsInitialGas).toEqual(mintAmount); - const tx = await bobsDeployMethod.send({ from: AztecAddress.ZERO, wait: { returnReceipt: true } }); + const tx = await bobsDeployMethod.send({ + from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [bobsAddress], + wait: { returnReceipt: true }, + }); expect(tx.transactionFee!).toBeGreaterThan(0n); await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([bobsInitialGas - tx.transactionFee!]); @@ -100,6 +105,8 @@ describe('e2e_fees account_init', () => { const paymentMethod = new FeeJuicePaymentMethodWithClaim(bobsAddress, claim); const tx = await bobsDeployMethod.send({ from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [bobsAddress], fee: { paymentMethod }, wait: { returnReceipt: true }, }); @@ -120,6 +127,8 @@ describe('e2e_fees account_init', () => { const paymentMethod = new PrivateFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const tx = await bobsDeployMethod.send({ from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [bobsAddress], fee: { paymentMethod }, wait: { returnReceipt: true }, }); @@ -149,6 +158,8 @@ describe('e2e_fees account_init', () => { const paymentMethod = new PublicFeePaymentMethod(bananaFPC.address, bobsAddress, wallet, gasSettings); const tx = await bobsDeployMethod.send({ from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [bobsAddress], skipInstancePublication: false, fee: { paymentMethod }, wait: { returnReceipt: true }, @@ -187,6 +198,8 @@ describe('e2e_fees account_init', () => { bobsSigningPubKey.y, ).send({ from: aliceAddress, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [bobsAddress], contractAddressSalt: bobsInstance.salt, skipClassPublication: true, skipInstancePublication: true, diff --git a/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts index f90dfadf2e7c..e55495e08e83 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fee_juice_payments.test.ts @@ -33,8 +33,9 @@ describe('e2e_fees Fee Juice payments', () => { // Alice pays for Bob's account contract deployment. const bobsDeployMethod = await bobsAccountManager.getDeployMethod(); - await bobsDeployMethod.send({ from: aliceAddress }); bobAddress = bobsAccountManager.address; + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + await bobsDeployMethod.send({ from: aliceAddress, additionalScopes: [bobAddress] }); }); afterAll(async () => { diff --git a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts index 3b01932f8edf..60960712c619 100644 --- a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts @@ -1,5 +1,5 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { Fr } from '@aztec/aztec.js/fields'; +import { Fr, GrumpkinScalar } from '@aztec/aztec.js/fields'; import type { Logger } from '@aztec/aztec.js/log'; import type { AztecNode } from '@aztec/aztec.js/node'; import { @@ -286,8 +286,19 @@ describe('e2e_pending_note_hashes_contract', () => { const minToNeedReset = Math.min(MAX_NOTE_HASHES_PER_TX, MAX_NOTE_HASH_READ_REQUESTS_PER_TX) + 1; const deployedContract = await deployContract(); + // We use 10 different recipients to send private logs to in order to avoid exceeding + // UNFINALIZED_TAGGING_INDEXES_WINDOW_LEN logs emitted for any single sender-recipient pair. + const recipients = ( + await Promise.all( + Array.from({ length: 10 }, () => + wallet.createSchnorrAccount(Fr.random(), Fr.random(), GrumpkinScalar.random()), + ), + ) + ).map(a => a.address); + await deployedContract.methods - .test_recursively_create_notes(owner, Math.ceil(minToNeedReset / notesPerIteration)) - .send({ from: owner }); + .test_recursively_create_notes(recipients, Math.ceil(minToNeedReset / notesPerIteration)) + // Recipients need to be in scope so their keys are accessible for note creation. + .send({ from: owner, additionalScopes: recipients }); }); }); diff --git a/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts b/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts index a5ed696875eb..708254faba14 100644 --- a/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts +++ b/yarn-project/end-to-end/src/e2e_public_testnet/e2e_public_testnet_transfer.test.ts @@ -62,6 +62,8 @@ describe(`deploys and transfers a private only token`, () => { await wallet.registerContract(tokenInstance, PrivateTokenContract.artifact, tokenSecretKey); const token = await tokenDeployment.send({ from: deployerAddress, + // The contract constructor initializes private storage vars that need the contract's own nullifier key. + additionalScopes: [tokenInstance.address], universalDeploy: true, skipInstancePublication: true, skipClassPublication: true, diff --git a/yarn-project/end-to-end/src/fixtures/setup.ts b/yarn-project/end-to-end/src/fixtures/setup.ts index 6def0cbc67f6..99a8a93fce63 100644 --- a/yarn-project/end-to-end/src/fixtures/setup.ts +++ b/yarn-project/end-to-end/src/fixtures/setup.ts @@ -841,6 +841,8 @@ export const deployAccounts = const deployMethod = await accountManager.getDeployMethod(); await deployMethod.send({ from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [accountManager.address], skipClassPublication: i !== 0, // Publish the contract class at most once. }); } diff --git a/yarn-project/end-to-end/src/fixtures/token_utils.ts b/yarn-project/end-to-end/src/fixtures/token_utils.ts index 47e9c67e78ba..aabcf5d6a215 100644 --- a/yarn-project/end-to-end/src/fixtures/token_utils.ts +++ b/yarn-project/end-to-end/src/fixtures/token_utils.ts @@ -25,8 +25,9 @@ export async function mintTokensToPrivate( minter: AztecAddress, recipient: AztecAddress, amount: bigint, + additionalScopes?: AztecAddress[], ) { - await token.methods.mint_to_private(recipient, amount).send({ from: minter }); + await token.methods.mint_to_private(recipient, amount).send({ from: minter, additionalScopes }); } export async function expectTokenBalance( diff --git a/yarn-project/end-to-end/src/shared/submit-transactions.ts b/yarn-project/end-to-end/src/shared/submit-transactions.ts index 5bb2a7316a8c..6b9eeb3fec42 100644 --- a/yarn-project/end-to-end/src/shared/submit-transactions.ts +++ b/yarn-project/end-to-end/src/shared/submit-transactions.ts @@ -19,7 +19,12 @@ export const submitTxsTo = async ( times(numTxs, async () => { const accountManager = await wallet.createSchnorrAccount(Fr.random(), Fr.random(), GrumpkinScalar.random()); const deployMethod = await accountManager.getDeployMethod(); - const txHash = await deployMethod.send({ from: submitter, wait: NO_WAIT }); + const txHash = await deployMethod.send({ + from: submitter, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [accountManager.address], + wait: NO_WAIT, + }); logger.info(`Tx sent with hash ${txHash}`); const receipt: TxReceipt = await wallet.getTxReceipt(txHash); diff --git a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts index c7567e794750..dddd719b1d18 100644 --- a/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts +++ b/yarn-project/end-to-end/src/spartan/setup_test_wallets.ts @@ -88,11 +88,23 @@ export async function deploySponsoredTestAccountsWithTokens( const paymentMethod = new SponsoredFeePaymentMethod(await getSponsoredFPCAddress()); const recipientDeployMethod = await recipientAccount.getDeployMethod(); - await recipientDeployMethod.send({ from: AztecAddress.ZERO, fee: { paymentMethod }, wait: { timeout: 2400 } }); + await recipientDeployMethod.send({ + from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [recipientAccount.address], + fee: { paymentMethod }, + wait: { timeout: 2400 }, + }); await Promise.all( fundedAccounts.map(async a => { const deployMethod = await a.getDeployMethod(); - await deployMethod.send({ from: AztecAddress.ZERO, fee: { paymentMethod }, wait: { timeout: 2400 } }); // increase timeout on purpose in order to account for two empty epochs + await deployMethod.send({ + from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [a.address], + fee: { paymentMethod }, + wait: { timeout: 2400 }, + }); // increase timeout on purpose in order to account for two empty epochs logger.info(`Account deployed at ${a.address}`); }), ); @@ -130,7 +142,13 @@ async function deployAccountWithDiagnostics( const deployMethod = await account.getDeployMethod(); let txHash; try { - txHash = await deployMethod.send({ from: AztecAddress.ZERO, fee: { paymentMethod }, wait: NO_WAIT }); + txHash = await deployMethod.send({ + from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [account.address], + fee: { paymentMethod }, + wait: NO_WAIT, + }); await waitForTx(aztecNode, txHash, { timeout: 2400 }); logger.info(`${accountLabel} deployed at ${account.address}`); } catch (error) { @@ -222,7 +240,8 @@ export async function deployTestAccountsWithTokens( fundedAccounts.map(async (a, i) => { const paymentMethod = new FeeJuicePaymentMethodWithClaim(a.address, claims[i]); const deployMethod = await a.getDeployMethod(); - await deployMethod.send({ from: AztecAddress.ZERO, fee: { paymentMethod } }); + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + await deployMethod.send({ from: AztecAddress.ZERO, additionalScopes: [a.address], fee: { paymentMethod } }); logger.info(`Account deployed at ${a.address}`); }), ); diff --git a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts index 9407c10950fe..24dc0adb6c85 100644 --- a/yarn-project/end-to-end/src/test-wallet/test_wallet.ts +++ b/yarn-project/end-to-end/src/test-wallet/test_wallet.ts @@ -275,7 +275,7 @@ export class TestWallet extends BaseWallet { async proveTx(exec: ExecutionPayload, opts: Omit): Promise { const fee = await this.completeFeeOptions(opts.from, exec.feePayer, opts.fee?.gasSettings); const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(exec, opts.from, fee); - const txProvingResult = await this.pxe.proveTx(txRequest, this.scopesFor(opts.from)); + const txProvingResult = await this.pxe.proveTx(txRequest, this.scopesFrom(opts.from, opts.additionalScopes)); return new ProvenTx( this.aztecNode, await txProvingResult.toTx(), diff --git a/yarn-project/prover-node/src/job/epoch-proving-job.test.ts b/yarn-project/prover-node/src/job/epoch-proving-job.test.ts index 93a5920cb80d..c94818623302 100644 --- a/yarn-project/prover-node/src/job/epoch-proving-job.test.ts +++ b/yarn-project/prover-node/src/job/epoch-proving-job.test.ts @@ -134,7 +134,7 @@ describe('epoch-proving-job', () => { publicProcessor.process.mockImplementation(async txs => { const txsArray = await toArray(txs); const processedTxs = await Promise.all(txsArray.map(tx => mock({ hash: tx.getTxHash() }))); - return [processedTxs, [], txsArray, [], 0]; + return [processedTxs, [], txsArray, [], 0, []]; }); }); @@ -179,7 +179,7 @@ describe('epoch-proving-job', () => { publicProcessor.process.mockImplementation(async txs => { const txsArray = await toArray(txs); const errors = txsArray.map(tx => ({ error: new Error('Failed to process tx'), tx })); - return [[], errors, [], [], 0]; + return [[], errors, [], [], 0, []]; }); const job = createJob(); @@ -190,7 +190,7 @@ describe('epoch-proving-job', () => { }); it('fails if does not process all txs for a block', async () => { - publicProcessor.process.mockImplementation(_txs => Promise.resolve([[], [], [], [], 0])); + publicProcessor.process.mockImplementation(_txs => Promise.resolve([[], [], [], [], 0, []])); const job = createJob(); await job.run(); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index 7c72b6407c86..f49faf4a03e2 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -527,22 +527,13 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP isStaticCall = isStaticCall || this.callContext.isStaticCall; - // When scopes are set and the target contract is a registered account (has keys in the keyStore), - // expand scopes to include it so nested private calls can sync and read the contract's own notes. - // We only expand for registered accounts because the log service needs the recipient's keys to derive - // tagging secrets, which are only available for registered accounts. - const expandedScopes = - this.scopes !== 'ALL_SCOPES' && (await this.keyStore.hasAccount(targetContractAddress)) - ? [...this.scopes, targetContractAddress] - : this.scopes; - await this.contractSyncService.ensureContractSynced( targetContractAddress, functionSelector, this.utilityExecutor, this.anchorBlockHeader, this.jobId, - expandedScopes, + this.scopes, ); const targetArtifact = await this.contractStore.getFunctionArtifactWithDebugMetadata( @@ -580,7 +571,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP totalPublicCalldataCount: this.totalPublicCalldataCount, sideEffectCounter, log: this.log, - scopes: expandedScopes, + scopes: this.scopes, senderForTags: this.senderForTags, simulator: this.simulator!, }); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts index 00242b8bb925..f957d44326a8 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts @@ -3,7 +3,7 @@ import type { BlockNumber } from '@aztec/foundation/branded-types'; import { Aes128 } from '@aztec/foundation/crypto/aes128'; import { Fr } from '@aztec/foundation/curves/bn254'; import { Point } from '@aztec/foundation/curves/grumpkin'; -import { LogLevels, type Logger, applyStringFormatting, createLogger } from '@aztec/foundation/log'; +import { LogLevels, type Logger, createLogger } from '@aztec/foundation/log'; import type { MembershipWitness } from '@aztec/foundation/trees'; import type { KeyStore } from '@aztec/key-store'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; @@ -21,6 +21,7 @@ import { MerkleTreeId, type NullifierMembershipWitness, PublicDataWitness } from import type { BlockHeader, Capsule } from '@aztec/stdlib/tx'; import type { AccessScopes } from '../../access_scopes.js'; +import { createContractLogger, logContractMessage } from '../../contract_logging.js'; import { EventService } from '../../events/event_service.js'; import { LogService } from '../../logs/log_service.js'; import { NoteService } from '../../notes/note_service.js'; @@ -402,12 +403,13 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra */ async #getContractLogger(): Promise { if (!this.contractLogger) { - const addrAbbrev = this.contractAddress.toString().slice(0, 10); - const name = await this.contractStore.getDebugContractName(this.contractAddress); - const module = name ? `contract_log::${name}(${addrAbbrev})` : `contract_log::${addrAbbrev}`; // Purpose of instanceId is to distinguish logs from different instances of the same component. It makes sense // to re-use jobId as instanceId here as executions of different PXE jobs are isolated. - this.contractLogger = createLogger(module, { instanceId: this.jobId }); + this.contractLogger = await createContractLogger( + this.contractAddress, + addr => this.contractStore.getDebugContractName(addr), + { instanceId: this.jobId }, + ); } return this.contractLogger; } @@ -416,9 +418,8 @@ export class UtilityExecutionOracle implements IMiscOracle, IUtilityExecutionOra if (!LogLevels[level]) { throw new Error(`Invalid log level: ${level}`); } - const levelName = LogLevels[level]; const logger = await this.#getContractLogger(); - logger[levelName](`${applyStringFormatting(message, fields)}`); + logContractMessage(logger, LogLevels[level], message, fields); } public async utilityFetchTaggedLogs(pendingTaggedLogArrayBaseSlot: Fr) { diff --git a/yarn-project/pxe/src/contract_logging.ts b/yarn-project/pxe/src/contract_logging.ts new file mode 100644 index 000000000000..cb32e2026fa1 --- /dev/null +++ b/yarn-project/pxe/src/contract_logging.ts @@ -0,0 +1,39 @@ +import type { Fr } from '@aztec/foundation/curves/bn254'; +import { type LogLevel, type Logger, applyStringFormatting, createLogger } from '@aztec/foundation/log'; +import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { DebugLog } from '@aztec/stdlib/logs'; + +/** Resolves a contract address to a human-readable name, if available. */ +export type ContractNameResolver = (address: AztecAddress) => Promise; + +/** + * Creates a logger whose output is prefixed with `contract_log::()`. + */ +export async function createContractLogger( + contractAddress: AztecAddress, + getContractName: ContractNameResolver, + options?: { instanceId?: string }, +): Promise { + const addrAbbrev = contractAddress.toString().slice(0, 10); + const name = await getContractName(contractAddress); + const module = name ? `contract_log::${name}(${addrAbbrev})` : `contract_log::Unknown(${addrAbbrev})`; + return createLogger(module, options); +} + +/** + * Formats and emits a single contract log message through the given logger. + */ +export function logContractMessage(logger: Logger, level: LogLevel, message: string, fields: Fr[]): void { + logger[level](applyStringFormatting(message, fields)); +} + +/** + * Displays debug logs collected during public function simulation, + * using the `contract_log::` prefixed logger format. + */ +export async function displayDebugLogs(debugLogs: DebugLog[], getContractName: ContractNameResolver): Promise { + for (const log of debugLogs) { + const logger = await createContractLogger(log.contractAddress, getContractName); + logContractMessage(logger, log.level, log.message, log.fields); + } +} diff --git a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts index e532ec2b7b8a..d854f0abf873 100644 --- a/yarn-project/pxe/src/entrypoints/client/bundle/index.ts +++ b/yarn-project/pxe/src/entrypoints/client/bundle/index.ts @@ -3,6 +3,7 @@ export * from '../../../notes_filter.js'; export * from '../../../pxe.js'; export * from '../../../config/index.js'; export * from '../../../error_enriching.js'; +export * from '../../../contract_logging.js'; export * from '../../../storage/index.js'; export * from './utils.js'; export type { PXECreationOptions } from '../../pxe_creation_options.js'; diff --git a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts index 5efe9b4e4ec6..17b4025cbf74 100644 --- a/yarn-project/pxe/src/entrypoints/client/lazy/index.ts +++ b/yarn-project/pxe/src/entrypoints/client/lazy/index.ts @@ -4,5 +4,6 @@ export * from '../../../pxe.js'; export * from '../../../config/index.js'; export * from '../../../storage/index.js'; export * from '../../../error_enriching.js'; +export * from '../../../contract_logging.js'; export * from './utils.js'; export { type PXECreationOptions } from '../../pxe_creation_options.js'; diff --git a/yarn-project/pxe/src/pxe.ts b/yarn-project/pxe/src/pxe.ts index 206809206590..d3819da91aad 100644 --- a/yarn-project/pxe/src/pxe.ts +++ b/yarn-project/pxe/src/pxe.ts @@ -61,6 +61,7 @@ import { generateSimulatedProvingResult, } from './contract_function_simulator/contract_function_simulator.js'; import { ProxiedContractStoreFactory } from './contract_function_simulator/proxied_contract_data_source.js'; +import { displayDebugLogs } from './contract_logging.js'; import { ContractSyncService } from './contract_sync/contract_sync_service.js'; import { readCurrentClassId } from './contract_sync/helpers.js'; import { PXEDebugUtils } from './debug/pxe_debug_utils.js'; @@ -947,6 +948,9 @@ export class PXE { const publicSimulationTimer = new Timer(); publicOutput = await this.#simulatePublicCalls(simulatedTx, skipFeeEnforcement); publicSimulationTime = publicSimulationTimer.ms(); + if (publicOutput?.debugLogs?.length) { + await displayDebugLogs(publicOutput.debugLogs, addr => this.contractStore.getDebugContractName(addr)); + } } let validationTime: number | undefined; diff --git a/yarn-project/simulator/src/public/public_processor/public_processor.ts b/yarn-project/simulator/src/public/public_processor/public_processor.ts index 92698d928a20..74b83fdb9f96 100644 --- a/yarn-project/simulator/src/public/public_processor/public_processor.ts +++ b/yarn-project/simulator/src/public/public_processor/public_processor.ts @@ -25,6 +25,7 @@ import type { PublicProcessorValidator, SequencerConfig, } from '@aztec/stdlib/interfaces/server'; +import type { DebugLog } from '@aztec/stdlib/logs'; import { ProvingRequestType } from '@aztec/stdlib/proofs'; import { MerkleTreeId } from '@aztec/stdlib/trees'; import { @@ -130,7 +131,6 @@ class PublicProcessorTimeoutError extends Error { */ export class PublicProcessor implements Traceable { private metrics: PublicProcessorMetrics; - constructor( protected globalVariables: GlobalVariables, private guardedMerkleTree: GuardedMerkleTreeOperations, @@ -159,12 +159,13 @@ export class PublicProcessor implements Traceable { txs: Iterable | AsyncIterable, limits: PublicProcessorLimits = {}, validator: PublicProcessorValidator = {}, - ): Promise<[ProcessedTx[], FailedTx[], Tx[], NestedProcessReturnValues[], number]> { + ): Promise<[ProcessedTx[], FailedTx[], Tx[], NestedProcessReturnValues[], number, DebugLog[]]> { const { maxTransactions, maxBlockSize, deadline, maxBlockGas, maxBlobFields } = limits; const { preprocessValidator, nullifierCache } = validator; const result: ProcessedTx[] = []; const usedTxs: Tx[] = []; const failed: FailedTx[] = []; + const debugLogs: DebugLog[] = []; const timer = new Timer(); let totalSizeInBytes = 0; @@ -241,7 +242,7 @@ export class PublicProcessor implements Traceable { this.contractsDB.createCheckpoint(); try { - const [processedTx, returnValues] = await this.processTx(tx, deadline); + const [processedTx, returnValues, txDebugLogs] = await this.processTx(tx, deadline); // Inject a fake processing failure after N txs if requested const fakeThrowAfter = this.opts.fakeThrowAfterProcessingTxCount; @@ -290,6 +291,7 @@ export class PublicProcessor implements Traceable { result.push(processedTx); usedTxs.push(tx); returns = returns.concat(returnValues); + debugLogs.push(...txDebugLogs); totalPublicGas = totalPublicGas.add(processedTx.gasUsed.publicGas); totalBlockGas = totalBlockGas.add(processedTx.gasUsed.totalGas); @@ -363,7 +365,7 @@ export class PublicProcessor implements Traceable { totalSizeInBytes, }); - return [result, failed, usedTxs, returns, totalBlobFields]; + return [result, failed, usedTxs, returns, totalBlobFields, debugLogs]; } private async checkWorldStateUnchanged( @@ -383,8 +385,13 @@ export class PublicProcessor implements Traceable { } @trackSpan('PublicProcessor.processTx', tx => ({ [Attributes.TX_HASH]: tx.getTxHash().toString() })) - private async processTx(tx: Tx, deadline: Date | undefined): Promise<[ProcessedTx, NestedProcessReturnValues[]]> { - const [time, [processedTx, returnValues]] = await elapsed(() => this.processTxWithinDeadline(tx, deadline)); + private async processTx( + tx: Tx, + deadline: Date | undefined, + ): Promise<[ProcessedTx, NestedProcessReturnValues[], DebugLog[]]> { + const [time, [processedTx, returnValues, debugLogs]] = await elapsed(() => + this.processTxWithinDeadline(tx, deadline), + ); this.log.verbose( !tx.hasPublicCalls() @@ -407,7 +414,7 @@ export class PublicProcessor implements Traceable { }, ); - return [processedTx, returnValues ?? []]; + return [processedTx, returnValues ?? [], debugLogs]; } private async doTreeInsertionsForPrivateOnlyTx(processedTx: ProcessedTx): Promise { @@ -441,10 +448,9 @@ export class PublicProcessor implements Traceable { private async processTxWithinDeadline( tx: Tx, deadline: Date | undefined, - ): Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined]> { - const innerProcessFn: () => Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined]> = tx.hasPublicCalls() - ? () => this.processTxWithPublicCalls(tx) - : () => this.processPrivateOnlyTx(tx); + ): Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined, DebugLog[]]> { + const innerProcessFn: () => Promise<[ProcessedTx, NestedProcessReturnValues[] | undefined, DebugLog[]]> = + tx.hasPublicCalls() ? () => this.processTxWithPublicCalls(tx) : () => this.processPrivateOnlyTx(tx); // Fake a delay per tx if instructed (used for tests) const fakeDelayPerTxMs = this.opts.fakeProcessingDelayPerTxMs; @@ -512,7 +518,7 @@ export class PublicProcessor implements Traceable { @trackSpan('PublicProcessor.processPrivateOnlyTx', (tx: Tx) => ({ [Attributes.TX_HASH]: tx.getTxHash().toString(), })) - private async processPrivateOnlyTx(tx: Tx): Promise<[ProcessedTx, undefined]> { + private async processPrivateOnlyTx(tx: Tx): Promise<[ProcessedTx, undefined, DebugLog[]]> { const gasFees = this.globalVariables.gasFees; const transactionFee = computeTransactionFee(gasFees, tx.data.constants.txContext.gasSettings, tx.data.gasUsed); @@ -537,13 +543,13 @@ export class PublicProcessor implements Traceable { await this.contractsDB.addNewContracts(tx); - return [processedTx, undefined]; + return [processedTx, undefined, []]; } @trackSpan('PublicProcessor.processTxWithPublicCalls', tx => ({ [Attributes.TX_HASH]: tx.getTxHash().toString(), })) - private async processTxWithPublicCalls(tx: Tx): Promise<[ProcessedTx, NestedProcessReturnValues[]]> { + private async processTxWithPublicCalls(tx: Tx): Promise<[ProcessedTx, NestedProcessReturnValues[], DebugLog[]]> { const timer = new Timer(); const result = await this.publicTxSimulator.simulate(tx); @@ -581,7 +587,7 @@ export class PublicProcessor implements Traceable { revertReason, ); - return [processedTx, appLogicReturnValues]; + return [processedTx, appLogicReturnValues, result.logs ?? []]; } /** diff --git a/yarn-project/stdlib/src/tx/public_simulation_output.ts b/yarn-project/stdlib/src/tx/public_simulation_output.ts index 20e5d743e3d2..984a747fcf3d 100644 --- a/yarn-project/stdlib/src/tx/public_simulation_output.ts +++ b/yarn-project/stdlib/src/tx/public_simulation_output.ts @@ -7,6 +7,7 @@ import { z } from 'zod'; import { SimulationError } from '../errors/simulation_error.js'; import { Gas } from '../gas/gas.js'; import type { GasUsed } from '../gas/gas_used.js'; +import { DebugLog } from '../logs/debug_log.js'; import { NullishToUndefined } from '../schemas/schemas.js'; import { TxEffect } from '../tx/tx_effect.js'; import { GlobalVariables } from './global_variables.js'; @@ -71,6 +72,7 @@ export class PublicSimulationOutput { public txEffect: TxEffect, public publicReturnValues: NestedProcessReturnValues[], public gasUsed: GasUsed, + public debugLogs: DebugLog[] = [], ) {} static get schema(): ZodFor { @@ -86,6 +88,7 @@ export class PublicSimulationOutput { publicGas: Gas.schema, billedGas: Gas.schema, }), + debugLogs: z.array(DebugLog.schema).default([]), }) .transform( fields => @@ -95,6 +98,7 @@ export class PublicSimulationOutput { fields.txEffect, fields.publicReturnValues, fields.gasUsed, + fields.debugLogs, ), ); } diff --git a/yarn-project/validator-client/src/checkpoint_builder.test.ts b/yarn-project/validator-client/src/checkpoint_builder.test.ts index 76899d131bdd..38945d92aa4e 100644 --- a/yarn-project/validator-client/src/checkpoint_builder.test.ts +++ b/yarn-project/validator-client/src/checkpoint_builder.test.ts @@ -95,6 +95,7 @@ describe('CheckpointBuilder', () => { [], // usedTxs [], // returnValues 0, // usedTxBlobFields + [], // debugLogs ]); const result = await checkpointBuilder.buildBlock([], blockNumber, 1000n); @@ -118,6 +119,7 @@ describe('CheckpointBuilder', () => { [], // usedTxs [], // returnValues 0, // usedTxBlobFields + [], // debugLogs ]); const result = await checkpointBuilder.buildBlock([], blockNumber, 1000n); @@ -137,6 +139,7 @@ describe('CheckpointBuilder', () => { [], // usedTxs [], // returnValues 0, // usedTxBlobFields + [], // debugLogs ]); await expect(checkpointBuilder.buildBlock([], blockNumber, 1000n)).rejects.toThrow(NoValidTxsError); diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts index ac3983ce72ae..2b2c4cc5e300 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.test.ts @@ -88,6 +88,7 @@ describe('BaseWallet', () => { txEffect: TxEffect.empty(), publicReturnValues: [optimizedRv0, optimizedRv1], gasUsed: { totalGas: Gas.empty(), teardownGas: Gas.empty(), publicGas: Gas.empty(), billedGas: Gas.empty() }, + debugLogs: [], }; node.simulatePublicCalls.mockResolvedValue(optimizedPublicOutput); @@ -98,6 +99,7 @@ describe('BaseWallet', () => { txEffect: TxEffect.empty(), publicReturnValues: [normalRv0], gasUsed: { totalGas: Gas.empty(), teardownGas: Gas.empty(), publicGas: Gas.empty(), billedGas: Gas.empty() }, + debugLogs: [], }; const normalResult = new TxSimulationResult( mock(), diff --git a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts index d3d6ffc90185..d6ac9462f8be 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/base_wallet.ts @@ -28,7 +28,7 @@ import type { ChainInfo } from '@aztec/entrypoints/interfaces'; import { Fr } from '@aztec/foundation/curves/bn254'; import { createLogger } from '@aztec/foundation/log'; import type { FieldsOf } from '@aztec/foundation/types'; -import type { AccessScopes } from '@aztec/pxe/client/lazy'; +import type { AccessScopes, ContractNameResolver } from '@aztec/pxe/client/lazy'; import type { PXE, PackedPrivateEvent } from '@aztec/pxe/server'; import { type ContractArtifact, @@ -37,7 +37,7 @@ import { decodeFromAbi, } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; -import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ContractInstanceWithAddress, computePartialAddress, @@ -89,10 +89,10 @@ export abstract class BaseWallet implements Wallet { protected log = createLogger('wallet-sdk:base_wallet'), ) {} - // When `from` is the zero address (e.g. when deploying a new account contract), we return an - // empty scope list which acts as deny-all: no notes are visible and no keys are accessible. - protected scopesFor(from: AztecAddress): AztecAddress[] { - return from.isZero() ? [] : [from]; + protected scopesFrom(from: AztecAddress, additionalScopes: AztecAddress[] = []): AztecAddress[] { + const allScopes = from.isZero() ? additionalScopes : [from, ...additionalScopes]; + const scopeSet = new Set(allScopes.map(address => address.toString())); + return [...scopeSet].map(AztecAddress.fromString); } protected abstract getAccountFromAddress(address: AztecAddress): Promise; @@ -338,6 +338,15 @@ export abstract class BaseWallet implements Wallet { blockHeader = (await this.aztecNode.getBlockHeader())!; } + const getContractName: ContractNameResolver = async address => { + const instance = await this.pxe.getContractInstance(address); + if (!instance) { + return undefined; + } + const artifact = await this.pxe.getContractArtifact(instance.currentContractClassId); + return artifact?.name; + }; + const [optimizedResults, normalResult] = await Promise.all([ optimizableCalls.length > 0 ? simulateViaNode( @@ -348,6 +357,7 @@ export abstract class BaseWallet implements Wallet { feeOptions.gasSettings, blockHeader, opts.skipFeeEnforcement ?? true, + getContractName, ) : Promise.resolve([]), remainingCalls.length > 0 @@ -355,7 +365,7 @@ export abstract class BaseWallet implements Wallet { remainingPayload, opts.from, feeOptions, - this.scopesFor(opts.from), + this.scopesFrom(opts.from, opts.additionalScopes), opts.skipTxValidation, opts.skipFeeEnforcement ?? true, ) @@ -371,7 +381,7 @@ export abstract class BaseWallet implements Wallet { return this.pxe.profileTx(txRequest, { profileMode: opts.profileMode, skipProofGeneration: opts.skipProofGeneration ?? true, - scopes: this.scopesFor(opts.from), + scopes: this.scopesFrom(opts.from, opts.additionalScopes), }); } @@ -381,7 +391,7 @@ export abstract class BaseWallet implements Wallet { ): Promise> { const feeOptions = await this.completeFeeOptions(opts.from, executionPayload.feePayer, opts.fee?.gasSettings); const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, opts.from, feeOptions); - const provenTx = await this.pxe.proveTx(txRequest, this.scopesFor(opts.from)); + const provenTx = await this.pxe.proveTx(txRequest, this.scopesFrom(opts.from, opts.additionalScopes)); const tx = await provenTx.toTx(); const txHash = tx.getTxHash(); if (await this.aztecNode.getTxEffect(txHash)) { diff --git a/yarn-project/wallet-sdk/src/base-wallet/utils.ts b/yarn-project/wallet-sdk/src/base-wallet/utils.ts index 81737e7674dc..24153108d8ce 100644 --- a/yarn-project/wallet-sdk/src/base-wallet/utils.ts +++ b/yarn-project/wallet-sdk/src/base-wallet/utils.ts @@ -4,6 +4,8 @@ import type { ChainInfo } from '@aztec/entrypoints/interfaces'; import { makeTuple } from '@aztec/foundation/array'; import { Fr } from '@aztec/foundation/curves/bn254'; import type { Tuple } from '@aztec/foundation/serialize'; +import type { ContractNameResolver } from '@aztec/pxe/client/lazy'; +import { displayDebugLogs } from '@aztec/pxe/client/lazy'; import { generateSimulatedProvingResult } from '@aztec/pxe/simulator'; import { type FunctionCall, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; @@ -72,6 +74,7 @@ async function simulateBatchViaNode( gasSettings: GasSettings, blockHeader: BlockHeader, skipFeeEnforcement: boolean, + getContractName: ContractNameResolver, ): Promise { const txContext = new TxContext(chainInfo.chainId, chainInfo.version, gasSettings); @@ -145,6 +148,9 @@ async function simulateBatchViaNode( throw publicOutput.revertReason; } + // Display debug logs from the public simulation. + await displayDebugLogs(publicOutput.debugLogs, getContractName); + return new TxSimulationResult(privateResult, provingResult.publicInputs, publicOutput, undefined); } @@ -169,6 +175,7 @@ export async function simulateViaNode( gasSettings: GasSettings, blockHeader: BlockHeader, skipFeeEnforcement: boolean = true, + getContractName: ContractNameResolver, ): Promise { const batches: FunctionCall[][] = []; @@ -187,6 +194,7 @@ export async function simulateViaNode( gasSettings, blockHeader, skipFeeEnforcement, + getContractName, ); results.push(result); } diff --git a/yarn-project/wallets/src/testing.ts b/yarn-project/wallets/src/testing.ts index 3b2723cde5b6..64838bfec198 100644 --- a/yarn-project/wallets/src/testing.ts +++ b/yarn-project/wallets/src/testing.ts @@ -22,6 +22,8 @@ export async function deployFundedSchnorrAccounts( const deployMethod = await accountManager.getDeployMethod(); await deployMethod.send({ from: AztecAddress.ZERO, + // The account constructor initializes storage vars that need the contract's own nullifier key, so we need to add it to scopes. + additionalScopes: [accountManager.address], skipClassPublication: i !== 0, wait: waitOptions, });