-
Notifications
You must be signed in to change notification settings - Fork 613
feat(txe): allow authorizing cross-contract utility calls in nr tests #23064
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
nchamo
merged 3 commits into
merge-train/fairies
from
nchamo/f-638-allow-devs-to-authorize-cross-contract-utility-calls-from
May 8, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -132,21 +132,22 @@ struct UtilityContextOptions { | |
| /// ```noir | ||
| /// env.call_private_opts(from, CallPrivateOptions::new().with_additional_scopes([other]), call); | ||
| /// ``` | ||
| pub struct CallPrivateOptions<let N: u32> { | ||
| pub struct CallPrivateOptions<let N: u32, let T: u32> { | ||
| additional_scopes: [AztecAddress; N], | ||
| authorized_utility_call_targets: [AztecAddress; T], | ||
| } | ||
|
|
||
| impl CallPrivateOptions<0> { | ||
| impl CallPrivateOptions<0, 0> { | ||
| /// Creates default `CallPrivateOptions`. | ||
| /// | ||
| /// The default values are the same as if using [`TestEnvironment::call_private`] instead of | ||
| /// [`TestEnvironment::call_private_opts`]. | ||
| pub fn new() -> Self { | ||
| CallPrivateOptions { additional_scopes: [] } | ||
| CallPrivateOptions { additional_scopes: [], authorized_utility_call_targets: [] } | ||
| } | ||
| } | ||
|
|
||
| impl<let N: u32> CallPrivateOptions<N> { | ||
| impl<let N: u32, let T: u32> CallPrivateOptions<N, T> { | ||
| /// Grants access to secrets of additional accounts. | ||
| /// | ||
| /// By default only the secrets that belong to the `from` account can be accessed: this function lets contracts | ||
|
|
@@ -155,10 +156,27 @@ impl<let N: u32> CallPrivateOptions<N> { | |
| /// Example usage includes accounts withdrawing from an escrow, which may require accessing the escrow's own notes | ||
| /// and secrets. | ||
| pub fn with_additional_scopes<let N_2: u32>( | ||
| _self: Self, | ||
| self, | ||
| additional_scopes: [AztecAddress; N_2], | ||
| ) -> CallPrivateOptions<N_2> { | ||
| CallPrivateOptions { additional_scopes } | ||
| ) -> CallPrivateOptions<N_2, T> { | ||
| CallPrivateOptions { | ||
| additional_scopes, | ||
| authorized_utility_call_targets: self.authorized_utility_call_targets, | ||
| } | ||
| } | ||
|
|
||
| /// Authorizes cross-contract utility calls to the given target contracts during this call. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. loooooooooooooooooooooooooooooooong |
||
| /// | ||
| /// By default, cross-contract utility calls are denied. This method lets the test author specify which target | ||
| /// contracts are allowed to be called via utility functions during execution. | ||
| pub fn with_authorized_utility_call_targets<let T_2: u32>( | ||
| self, | ||
| targets: [AztecAddress; T_2], | ||
| ) -> CallPrivateOptions<N, T_2> { | ||
| CallPrivateOptions { | ||
| additional_scopes: self.additional_scopes, | ||
| authorized_utility_call_targets: targets, | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -169,21 +187,22 @@ impl<let N: u32> CallPrivateOptions<N> { | |
| /// ```noir | ||
| /// env.view_private_opts(from, ViewPrivateOptions::new().with_additional_scopes([other]), call); | ||
| /// ``` | ||
| pub struct ViewPrivateOptions<let S: u32> { | ||
| pub struct ViewPrivateOptions<let S: u32, let T: u32> { | ||
| additional_scopes: [AztecAddress; S], | ||
| authorized_utility_call_targets: [AztecAddress; T], | ||
| } | ||
|
|
||
| impl ViewPrivateOptions<0> { | ||
| impl ViewPrivateOptions<0, 0> { | ||
| /// Creates default `ViewPrivateOptions`. | ||
| /// | ||
| /// The default values are the same as if using [`TestEnvironment::view_private`] instead of | ||
| /// [`TestEnvironment::view_private_opts`]. | ||
| pub fn new() -> Self { | ||
| ViewPrivateOptions { additional_scopes: [] } | ||
| ViewPrivateOptions { additional_scopes: [], authorized_utility_call_targets: [] } | ||
| } | ||
| } | ||
|
|
||
| impl<let S: u32> ViewPrivateOptions<S> { | ||
| impl<let S: u32, let T: u32> ViewPrivateOptions<S, T> { | ||
| /// Grants access to secrets of additional accounts. | ||
| /// | ||
| /// By default only the secrets that belong to the `from` account can be accessed: this function lets contracts | ||
|
|
@@ -192,10 +211,27 @@ impl<let S: u32> ViewPrivateOptions<S> { | |
| /// Example usage includes accounts querying an escrow's balance, which may require accessing the escrow's own | ||
| /// notes. | ||
| pub fn with_additional_scopes<let S2: u32>( | ||
| _self: Self, | ||
| self, | ||
| additional_scopes: [AztecAddress; S2], | ||
| ) -> ViewPrivateOptions<S2> { | ||
| ViewPrivateOptions { additional_scopes } | ||
| ) -> ViewPrivateOptions<S2, T> { | ||
| ViewPrivateOptions { | ||
| additional_scopes, | ||
| authorized_utility_call_targets: self.authorized_utility_call_targets, | ||
| } | ||
| } | ||
|
|
||
| /// Authorizes cross-contract utility calls to the given target contracts during this call. | ||
| /// | ||
| /// By default, cross-contract utility calls are denied. This method lets the test author specify which target | ||
| /// contracts are allowed to be called via utility functions during execution. | ||
| pub fn with_authorized_utility_call_targets<let T_2: u32>( | ||
| self, | ||
| targets: [AztecAddress; T_2], | ||
| ) -> ViewPrivateOptions<S, T_2> { | ||
| ViewPrivateOptions { | ||
| additional_scopes: self.additional_scopes, | ||
| authorized_utility_call_targets: targets, | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -207,6 +243,40 @@ struct EventDiscoveryOptions { | |
| contract_address: Option<AztecAddress>, | ||
| } | ||
|
|
||
| /// Configuration for [`TestEnvironment::execute_utility_opts`]. | ||
| /// | ||
| /// Constructed by calling [`ExecuteUtilityOptions::new`] and then chaining methods: | ||
| /// | ||
| /// ```noir | ||
| /// env.execute_utility_opts( | ||
| /// ExecuteUtilityOptions::new().with_authorized_utility_call_targets([target]), | ||
| /// SomeContract::at(addr).some_utility_function(), | ||
| /// ); | ||
| /// ``` | ||
| pub struct ExecuteUtilityOptions<let T: u32> { | ||
| authorized_utility_call_targets: [AztecAddress; T], | ||
| } | ||
|
|
||
| impl ExecuteUtilityOptions<0> { | ||
| /// Creates default `ExecuteUtilityOptions`. | ||
| pub fn new() -> Self { | ||
| ExecuteUtilityOptions { authorized_utility_call_targets: [] } | ||
| } | ||
| } | ||
|
|
||
| impl<let T: u32> ExecuteUtilityOptions<T> { | ||
| /// Authorizes cross-contract utility calls to the given target contracts during this call. | ||
| /// | ||
| /// By default, cross-contract utility calls are denied. This method lets the test author specify which target | ||
| /// contracts are allowed to be called via utility functions during execution. | ||
| pub fn with_authorized_utility_call_targets<let T_2: u32>( | ||
| _self: Self, | ||
| targets: [AztecAddress; T_2], | ||
| ) -> ExecuteUtilityOptions<T_2> { | ||
| ExecuteUtilityOptions { authorized_utility_call_targets: targets } | ||
| } | ||
| } | ||
|
|
||
| /// Configuration values for [`TestEnvironment::deploy_opts`]. Meant to be used by calling `new` and then chaining | ||
| /// methods setting each value, e.g.: | ||
| /// ```noir | ||
|
|
@@ -629,10 +699,10 @@ impl TestEnvironment { | |
| } | ||
|
|
||
| /// Variant of `call_private` which allows specifying multiple configuration values via `CallPrivateOptions`. | ||
| pub unconstrained fn call_private_opts<let M: u32, let N: u32, let S: u32, T>( | ||
| pub unconstrained fn call_private_opts<let M: u32, let N: u32, let S: u32, let U: u32, T>( | ||
| _self: Self, | ||
| from: AztecAddress, | ||
| opts: CallPrivateOptions<S>, | ||
| opts: CallPrivateOptions<S, U>, | ||
| call: PrivateCall<M, N, T>, | ||
| ) -> T | ||
| where | ||
|
|
@@ -646,6 +716,7 @@ impl TestEnvironment { | |
| hash_args(call.args), | ||
| /*is_static=*/ false, | ||
| opts.additional_scopes, | ||
| opts.authorized_utility_call_targets, | ||
| ); | ||
|
|
||
| T::deserialize(serialized_return_values) | ||
|
|
@@ -670,10 +741,10 @@ impl TestEnvironment { | |
| } | ||
|
|
||
| /// Variant of `view_private` which allows specifying multiple configuration values via `ViewPrivateOptions`. | ||
| pub unconstrained fn view_private_opts<let M: u32, let N: u32, let S: u32, T>( | ||
| pub unconstrained fn view_private_opts<let M: u32, let N: u32, let S: u32, let U: u32, T>( | ||
| _self: Self, | ||
| from: AztecAddress, | ||
| opts: ViewPrivateOptions<S>, | ||
| opts: ViewPrivateOptions<S, U>, | ||
| call: PrivateStaticCall<M, N, T>, | ||
| ) -> T | ||
| where | ||
|
|
@@ -687,6 +758,7 @@ impl TestEnvironment { | |
| hash_args(call.args), | ||
| /*is_static=*/ true, | ||
| opts.additional_scopes, | ||
| opts.authorized_utility_call_targets, | ||
| ); | ||
|
|
||
| T::deserialize(serialized_return_values) | ||
|
|
@@ -701,12 +773,32 @@ impl TestEnvironment { | |
| /// let contract_addr = env.deploy("SampleContract").without_initializer(); | ||
| /// let return_value = env.execute_utility(SampleContract::at(contract_addr).sample_utility_function()); | ||
| /// ``` | ||
| pub unconstrained fn execute_utility<let M: u32, let N: u32, T>(_self: Self, call: UtilityCall<M, N, T>) -> T | ||
| pub unconstrained fn execute_utility<let M: u32, let N: u32, T>( | ||
| self: Self, | ||
| call: UtilityCall<M, N, T>, | ||
| ) -> T | ||
| where | ||
| T: Deserialize, | ||
| { | ||
| let serialized_return_values = | ||
| txe_oracles::execute_utility_function(call.target_contract, call.selector, call.args); | ||
| self.execute_utility_opts(ExecuteUtilityOptions::new(), call) | ||
| } | ||
|
|
||
| /// Variant of `execute_utility` which allows specifying configuration values via `ExecuteUtilityOptions`, such as | ||
| /// authorizing cross-contract utility calls. | ||
| pub unconstrained fn execute_utility_opts<let M: u32, let N: u32, let U: u32, T>( | ||
| _self: Self, | ||
| opts: ExecuteUtilityOptions<U>, | ||
| call: UtilityCall<M, N, T>, | ||
| ) -> T | ||
| where | ||
| T: Deserialize, | ||
| { | ||
| let serialized_return_values = txe_oracles::execute_utility_function( | ||
| call.target_contract, | ||
| call.selector, | ||
| call.args, | ||
| opts.authorized_utility_call_targets, | ||
| ); | ||
|
|
||
| T::deserialize(serialized_return_values) | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
noir-projects/noir-contracts/contracts/test/nested_utility_contract/src/test.nr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| use crate::NestedUtility; | ||
| use aztec::{ | ||
| protocol::address::AztecAddress, | ||
| test::helpers::test_environment::{CallPrivateOptions, ExecuteUtilityOptions, TestEnvironment}, | ||
| }; | ||
|
|
||
| unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress) { | ||
| let mut env = TestEnvironment::new(); | ||
| let account = env.create_light_account(); | ||
| let addr_a = env.deploy("NestedUtility").without_initializer(); | ||
| let addr_b = env.deploy("NestedUtility").without_initializer(); | ||
| (env, account, addr_a, addr_b) | ||
| } | ||
|
|
||
| #[test] | ||
| unconstrained fn same_contract_utility_call_from_utility_succeeds() { | ||
| let (env, _, addr_a, _) = setup(); | ||
|
|
||
| let result: Field = env.execute_utility(NestedUtility::at(addr_a).pow_utility(2, 10)); | ||
| assert_eq(result, 1024); | ||
| } | ||
|
|
||
| #[test] | ||
| unconstrained fn same_contract_utility_call_from_private_succeeds() { | ||
| let (env, account, addr_a, _) = setup(); | ||
|
|
||
| let result: Field = | ||
| env.call_private(account, NestedUtility::at(addr_a).pow_private(2, 10)); | ||
| assert_eq(result, 1024); | ||
| } | ||
|
|
||
| #[test(should_fail_with = "Cross-contract utility call denied")] | ||
| unconstrained fn cross_contract_utility_call_from_utility_denied_by_default() { | ||
| let (env, _, addr_a, addr_b) = setup(); | ||
|
|
||
| let _: Field = env.execute_utility( | ||
| NestedUtility::at(addr_a).delegate_pow_utility(addr_b, 2, 3), | ||
| ); | ||
| } | ||
|
|
||
| #[test(should_fail_with = "Cross-contract utility call denied")] | ||
| unconstrained fn cross_contract_utility_call_from_private_denied_by_default() { | ||
| let (env, account, addr_a, addr_b) = setup(); | ||
|
|
||
| let _: Field = env.call_private( | ||
| account, | ||
| NestedUtility::at(addr_a).delegate_pow_private(addr_b, 2, 3), | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| unconstrained fn cross_contract_utility_call_from_utility_succeeds_with_authorization() { | ||
| let (env, _, addr_a, addr_b) = setup(); | ||
|
|
||
| let result: Field = env.execute_utility_opts( | ||
| ExecuteUtilityOptions::new().with_authorized_utility_call_targets([addr_b]), | ||
| NestedUtility::at(addr_a).delegate_pow_utility(addr_b, 2, 3), | ||
| ); | ||
| assert_eq(result, 8); | ||
| } | ||
|
|
||
| #[test] | ||
| unconstrained fn cross_contract_utility_call_from_private_succeeds_with_authorization() { | ||
| let (env, account, addr_a, addr_b) = setup(); | ||
|
|
||
| let result: Field = env.call_private_opts( | ||
| account, | ||
| CallPrivateOptions::new().with_authorized_utility_call_targets([addr_b]), | ||
| NestedUtility::at(addr_a).delegate_pow_private(addr_b, 2, 3), | ||
| ); | ||
| assert_eq(result, 8); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be docs (error page? idk) that tell you how to fix the issue when you run into it, incl. the fact that if in a test you can do this. They should also explain why this is not allowed by default.