diff --git a/docs/docs-developers/docs/aztec-nr/testing_contracts.md b/docs/docs-developers/docs/aztec-nr/testing_contracts.md index eeaa18bb94ae..d4cd8315e8c7 100644 --- a/docs/docs-developers/docs/aztec-nr/testing_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/testing_contracts.md @@ -41,7 +41,7 @@ aztec test 2. Run `aztec test` :::warning -Always use `aztec test` instead of `nargo test`. The `TestEnvironment` requires the TXE (Test eXecution Environment) oracle resolver. +Always use `aztec test` instead of `nargo test`. The `TestEnvironment` requires the test environment oracle resolver provided by the `aztec` CLI. ::: ## Basic test structure @@ -348,3 +348,25 @@ unconstrained fn test_missing_authwit() { } ``` + +## Test environment oracle versioning + +The test environment uses an oracle interface to communicate between your Noir test code and the `aztec test` CLI. This interface is versioned so that mismatches between the Aztec.nr dependency used to compile the test and the CLI version are detected automatically. + +The version uses two components, `major.minor`, with the same compatibility rules as [PXE oracle versioning](../foundational-topics/pxe/index.md#oracle-versioning): + +- **`major`** must match exactly. A major bump means oracles were removed or had their signatures changed, and a test environment on a different major cannot safely run the test. +- **`minor`** indicates additive changes (new oracles). The test environment uses a best-effort approach: a test compiled against a higher `minor` is still allowed to run, and an error is only thrown if the test actually invokes an oracle the test environment does not know about. + +### Resolving a version mismatch + +If you see an error like _"Incompatible test environment version: The test was compiled with a newer version of Aztec.nr than your test environment supports"_, the test uses oracles from a newer Aztec.nr than your `aztec test` CLI supports. + +To fix it, make sure your `aztec` CLI version and the `aztec` dependency in the test crate's `Nargo.toml` are on the same release. Note that the test crate's Aztec.nr version can differ from the contract crate's version, depending on your project configuration. For example, if your CLI is on `v4.3.0`, the test crate's `Nargo.toml` should reference the matching tag: + +```toml +[dependencies] +aztec = { git="https://github.com/AztecProtocol/aztec-nr", tag="v4.3.0", directory="aztec" } +``` + +If the test environment reports a version that _should_ include every oracle the test needs but an oracle is still missing, this is likely a bug rather than a version problem. diff --git a/docs/netlify.toml b/docs/netlify.toml index a80bea176f71..118de8c4f2bc 100644 --- a/docs/netlify.toml +++ b/docs/netlify.toml @@ -818,3 +818,13 @@ # PXE: cross-contract utility call denied by execution hook from = "/errors/11" to = "/developers/docs/aztec-nr/debugging#cross-contract-utility-call-denied" + +[[redirects]] + # Incompatible oracle version between test and test environment + from = "/errors/12" + to = "/developers/docs/aztec-nr/testing_contracts#test-environment-oracle-versioning" + +[[redirects]] + # Unexpected error: how to report issues + from = "/errors/13" + to = "/developers/docs/aztec-nr/debugging#reporting-issues" diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr index d4f2cb93b1da..bb853bbfc1e7 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -1,7 +1,8 @@ use crate::protocol::{ - abis::function_selector::FunctionSelector, + abis::{function_selector::FunctionSelector, gas::Gas, gas_fees::GasFees, gas_settings::GasSettings}, address::AztecAddress, - traits::{Deserialize, FromField, Packable, Serialize}, + constants::{MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS}, + traits::{Deserialize, Empty, FromField, Packable, Serialize}, }; use crate::{ @@ -91,14 +92,26 @@ pub struct TestEnvironment { pub struct PrivateContextOptions { contract_address: Option, anchor_block_number: Option, + gas_limits: Option, + teardown_gas_limits: Option, + max_fees_per_gas: Option, + max_priority_fees_per_gas: Option, } impl PrivateContextOptions { - /// Creates a new `PrivateContextOptions` with default values, i.e. the same as if using the `private_context` - /// method instead of `private_context_opts`. Use the `at_anchor_block_number` and other methods to set the desired + /// Creates a new `PrivateContextOptions` with default values, i.e. the same as if using + /// [`TestEnvironment::private_context`] instead of [`TestEnvironment::private_context_opts`]. Use + /// [`at_anchor_block_number`](PrivateContextOptions::at_anchor_block_number) and other methods to set the desired /// configuration values. pub fn new() -> Self { - Self { contract_address: Option::none(), anchor_block_number: Option::none() } + Self { + contract_address: Option::none(), + anchor_block_number: Option::none(), + gas_limits: Option::none(), + teardown_gas_limits: Option::none(), + max_fees_per_gas: Option::none(), + max_priority_fees_per_gas: Option::none(), + } } /// Sets the desired anchor block number to base the private context in. Only past block numbers can be specified. @@ -115,6 +128,61 @@ impl PrivateContextOptions { self.contract_address = Option::some(contract_address); *self } + + /// Sets the gas limits for the transaction. + /// + /// If not set, defaults to the maximum the protocol allows. + pub fn with_gas_limits(&mut self, gas_limits: Gas) -> Self { + self.gas_limits = Option::some(gas_limits); + *self + } + + /// Sets the teardown gas limits for the transaction. + /// + /// If not set, defaults to 1/8 of the protocol maximum. + pub fn with_teardown_gas_limits(&mut self, teardown_gas_limits: Gas) -> Self { + self.teardown_gas_limits = Option::some(teardown_gas_limits); + *self + } + + /// Sets the maximum fees per gas unit the sender is willing to pay. + /// + /// If not set, defaults to zero. + pub fn with_max_fees_per_gas(&mut self, max_fees_per_gas: GasFees) -> Self { + self.max_fees_per_gas = Option::some(max_fees_per_gas); + *self + } + + /// Sets the maximum priority fees per gas unit. + /// + /// If not set, defaults to zero. + pub fn with_max_priority_fees_per_gas(&mut self, max_priority_fees_per_gas: GasFees) -> Self { + self.max_priority_fees_per_gas = Option::some(max_priority_fees_per_gas); + *self + } + + /// Sets all gas settings at once. + /// + /// Equivalent to calling [`with_gas_limits`](PrivateContextOptions::with_gas_limits), + /// [`with_teardown_gas_limits`](PrivateContextOptions::with_teardown_gas_limits), + /// [`with_max_fees_per_gas`](PrivateContextOptions::with_max_fees_per_gas), and + /// [`with_max_priority_fees_per_gas`](PrivateContextOptions::with_max_priority_fees_per_gas) individually. + pub fn with_gas_settings(&mut self, gas_settings: GasSettings) -> Self { + self.gas_limits = Option::some(gas_settings.gas_limits); + self.teardown_gas_limits = Option::some(gas_settings.teardown_gas_limits); + self.max_fees_per_gas = Option::some(gas_settings.max_fees_per_gas); + self.max_priority_fees_per_gas = Option::some(gas_settings.max_priority_fees_per_gas); + *self + } + + fn resolve_gas_settings(self) -> GasSettings { + resolve_gas_settings( + self.gas_limits, + self.teardown_gas_limits, + self.max_fees_per_gas, + self.max_priority_fees_per_gas, + ) + } } struct PublicContextOptions { @@ -135,6 +203,10 @@ struct UtilityContextOptions { pub struct CallPrivateOptions { additional_scopes: [AztecAddress; N], authorized_utility_call_targets: [AztecAddress; T], + gas_limits: Option, + teardown_gas_limits: Option, + max_fees_per_gas: Option, + max_priority_fees_per_gas: Option, } impl CallPrivateOptions<0, 0> { @@ -143,7 +215,14 @@ impl CallPrivateOptions<0, 0> { /// 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: [], authorized_utility_call_targets: [] } + CallPrivateOptions { + additional_scopes: [], + authorized_utility_call_targets: [], + gas_limits: Option::none(), + teardown_gas_limits: Option::none(), + max_fees_per_gas: Option::none(), + max_priority_fees_per_gas: Option::none(), + } } } @@ -159,7 +238,14 @@ impl CallPrivateOptions { self, additional_scopes: [AztecAddress; N_2], ) -> CallPrivateOptions { - CallPrivateOptions { additional_scopes, authorized_utility_call_targets: self.authorized_utility_call_targets } + CallPrivateOptions { + additional_scopes, + authorized_utility_call_targets: self.authorized_utility_call_targets, + gas_limits: self.gas_limits, + teardown_gas_limits: self.teardown_gas_limits, + max_fees_per_gas: self.max_fees_per_gas, + max_priority_fees_per_gas: self.max_priority_fees_per_gas, + } } /// Authorizes cross-contract utility calls to the given target contracts during this call. @@ -170,7 +256,69 @@ impl CallPrivateOptions { self, targets: [AztecAddress; T_2], ) -> CallPrivateOptions { - CallPrivateOptions { additional_scopes: self.additional_scopes, authorized_utility_call_targets: targets } + CallPrivateOptions { + additional_scopes: self.additional_scopes, + authorized_utility_call_targets: targets, + gas_limits: self.gas_limits, + teardown_gas_limits: self.teardown_gas_limits, + max_fees_per_gas: self.max_fees_per_gas, + max_priority_fees_per_gas: self.max_priority_fees_per_gas, + } + } + + /// Sets the gas limits for the transaction. + /// + /// If not set, defaults to the maximum the protocol allows. + pub fn with_gas_limits(mut self, gas_limits: Gas) -> Self { + self.gas_limits = Option::some(gas_limits); + self + } + + /// Sets the teardown gas limits for the transaction. + /// + /// If not set, defaults to 1/8 of the protocol maximum. + pub fn with_teardown_gas_limits(mut self, teardown_gas_limits: Gas) -> Self { + self.teardown_gas_limits = Option::some(teardown_gas_limits); + self + } + + /// Sets the maximum fees per gas unit the sender is willing to pay. + /// + /// If not set, defaults to zero. + pub fn with_max_fees_per_gas(mut self, max_fees_per_gas: GasFees) -> Self { + self.max_fees_per_gas = Option::some(max_fees_per_gas); + self + } + + /// Sets the maximum priority fees per gas unit. + /// + /// If not set, defaults to zero. + pub fn with_max_priority_fees_per_gas(mut self, max_priority_fees_per_gas: GasFees) -> Self { + self.max_priority_fees_per_gas = Option::some(max_priority_fees_per_gas); + self + } + + /// Convenience method to set all four gas settings fields at once. + /// + /// Equivalent to calling [`with_gas_limits`](CallPrivateOptions::with_gas_limits), + /// [`with_teardown_gas_limits`](CallPrivateOptions::with_teardown_gas_limits), + /// [`with_max_fees_per_gas`](CallPrivateOptions::with_max_fees_per_gas), and + /// [`with_max_priority_fees_per_gas`](CallPrivateOptions::with_max_priority_fees_per_gas) individually. + pub fn with_gas_settings(mut self, gas_settings: GasSettings) -> Self { + self.gas_limits = Option::some(gas_settings.gas_limits); + self.teardown_gas_limits = Option::some(gas_settings.teardown_gas_limits); + self.max_fees_per_gas = Option::some(gas_settings.max_fees_per_gas); + self.max_priority_fees_per_gas = Option::some(gas_settings.max_priority_fees_per_gas); + self + } + + fn resolve_gas_settings(self) -> GasSettings { + resolve_gas_settings( + self.gas_limits, + self.teardown_gas_limits, + self.max_fees_per_gas, + self.max_priority_fees_per_gas, + ) } } @@ -297,10 +445,123 @@ impl DeployOptions { } } +/// Configuration values for [`TestEnvironment::call_public_opts`]. Meant to be used by calling `new` and then chaining +/// methods setting each value, e.g.: +/// ```noir +/// env.call_public_opts(caller, CallPublicOptions::new().with_max_fees_per_gas(GasFees::new(1, 1)), +/// MyContract::at(addr).fn()); +/// ``` +/// +/// Gas settings fields are not cross-validated - nonsense combinations (e.g. `teardown_gas_limits` larger than +/// `gas_limits`) will be forwarded as-is and may be rejected by simulation. +pub struct CallPublicOptions { + gas_limits: Option, + teardown_gas_limits: Option, + max_fees_per_gas: Option, + max_priority_fees_per_gas: Option, +} + +impl CallPublicOptions { + /// Creates a new `CallPublicOptions` with default values, i.e. the same as if using + /// [`TestEnvironment::call_public`] without the `_opts` variant. + pub fn new() -> Self { + Self { + gas_limits: Option::none(), + teardown_gas_limits: Option::none(), + max_fees_per_gas: Option::none(), + max_priority_fees_per_gas: Option::none(), + } + } + + /// Sets the gas limits for the transaction. + /// + /// If not set, defaults to the maximum the protocol allows. + pub fn with_gas_limits(&mut self, gas_limits: Gas) -> Self { + self.gas_limits = Option::some(gas_limits); + *self + } + + /// Sets the teardown gas limits for the transaction. + /// + /// If not set, defaults to 1/8 of the protocol maximum. + pub fn with_teardown_gas_limits(&mut self, teardown_gas_limits: Gas) -> Self { + self.teardown_gas_limits = Option::some(teardown_gas_limits); + *self + } + + /// Sets the maximum fees per gas unit the sender is willing to pay. + /// + /// If not set, defaults to zero. + pub fn with_max_fees_per_gas(&mut self, max_fees_per_gas: GasFees) -> Self { + self.max_fees_per_gas = Option::some(max_fees_per_gas); + *self + } + + /// Sets the maximum priority fees per gas unit. + /// + /// If not set, defaults to zero. + pub fn with_max_priority_fees_per_gas(&mut self, max_priority_fees_per_gas: GasFees) -> Self { + self.max_priority_fees_per_gas = Option::some(max_priority_fees_per_gas); + *self + } + + /// Convenience method to set all four gas settings fields at once. + /// + /// Equivalent to calling [`with_gas_limits`](CallPublicOptions::with_gas_limits), + /// [`with_teardown_gas_limits`](CallPublicOptions::with_teardown_gas_limits), + /// [`with_max_fees_per_gas`](CallPublicOptions::with_max_fees_per_gas), and + /// [`with_max_priority_fees_per_gas`](CallPublicOptions::with_max_priority_fees_per_gas) individually. + pub fn with_gas_settings(&mut self, gas_settings: GasSettings) -> Self { + self.gas_limits = Option::some(gas_settings.gas_limits); + self.teardown_gas_limits = Option::some(gas_settings.teardown_gas_limits); + self.max_fees_per_gas = Option::some(gas_settings.max_fees_per_gas); + self.max_priority_fees_per_gas = Option::some(gas_settings.max_priority_fees_per_gas); + *self + } + + fn resolve_gas_settings(self) -> GasSettings { + resolve_gas_settings( + self.gas_limits, + self.teardown_gas_limits, + self.max_fees_per_gas, + self.max_priority_fees_per_gas, + ) + } +} + +/// Resolves `GasSettings` from per-component options, applying defaults matching the TXE's built-in values when an +/// option is `None`. +fn resolve_gas_settings( + gas_limits: Option, + teardown_gas_limits: Option, + max_fees_per_gas: Option, + max_priority_fees_per_gas: Option, +) -> GasSettings { + GasSettings::new( + gas_limits + .unwrap_or( + Gas::new( + MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, + MAX_PROCESSABLE_L2_GAS, + ), + ), + teardown_gas_limits + .unwrap_or( + Gas::new( + MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT / 8, + MAX_PROCESSABLE_L2_GAS / 8, + ), + ), + max_fees_per_gas.unwrap_or(GasFees::empty()), + max_priority_fees_per_gas.unwrap_or(GasFees::empty()), + ) +} + impl TestEnvironment { /// Creates a new `TestEnvironment`. This function should only be called once per test. pub unconstrained fn new() -> Self { assert_compatible_oracle_version(); + txe_oracles::assert_compatible_txe_oracle_version(); Self { // Use an offset to avoid secret collision with account secrets. Without this, when // deploying multiple contracts and creating accounts, they could end up with identical @@ -356,8 +617,8 @@ impl TestEnvironment { self.public_context_opts(PublicContextOptions { contract_address: Option::none() }, f) } - /// Variant of `public_context` which allows specifying the contract address in which the public context will - /// execute, which will affect note and nullifier siloing, storage access, etc. + /// Variant of [`TestEnvironment::public_context`] which allows specifying the contract address in which the + /// public context will execute, which will affect note and nullifier siloing, storage access, etc. pub unconstrained fn public_context_at( self: Self, addr: AztecAddress, @@ -415,33 +676,32 @@ impl TestEnvironment { /// }); /// ``` pub unconstrained fn private_context(self: Self, f: unconstrained fn[Env](&mut PrivateContext) -> T) -> T { - self.private_context_opts( - PrivateContextOptions { contract_address: Option::none(), anchor_block_number: Option::none() }, - f, - ) + self.private_context_opts(PrivateContextOptions::new(), f) } - /// Variant of `private_context` which allows specifying the contract address in which the private context will - /// execute, which will affect note and nullifier siloing, storage access, etc. + /// Variant of [`TestEnvironment::private_context`] which allows specifying the contract address in which the + /// private context will execute, which will affect note and nullifier siloing, storage access, etc. pub unconstrained fn private_context_at( self: Self, addr: AztecAddress, f: unconstrained fn[Env](&mut PrivateContext) -> T, ) -> T { - self.private_context_opts( - PrivateContextOptions { contract_address: Option::some(addr), anchor_block_number: Option::none() }, - f, - ) + self.private_context_opts(PrivateContextOptions::new().at_contract_address(addr), f) } - /// Variant of `private_context` which allows specifying multiple configuration values via `PrivateContextOptions`. + /// Variant of [`TestEnvironment::private_context`] which allows specifying multiple configuration values via + /// [`PrivateContextOptions`]. pub unconstrained fn private_context_opts( _self: Self, opts: PrivateContextOptions, f: unconstrained fn[Env](&mut PrivateContext) -> T, ) -> T { let mut context = PrivateContext::new( - txe_oracles::set_private_txe_context(opts.contract_address, opts.anchor_block_number), + txe_oracles::set_private_txe_context( + opts.contract_address, + opts.anchor_block_number, + opts.resolve_gas_settings(), + ), 0, ); @@ -482,8 +742,8 @@ impl TestEnvironment { self.utility_context_opts(UtilityContextOptions { contract_address: Option::none() }, f) } - /// Variant of `utility_context` which allows specifying the contract address in which the utility context will - /// execute, which will affect note and storage access. + /// Variant of [`TestEnvironment::utility_context`] which allows specifying the contract address in which the + /// utility context will execute, which will affect note and storage access. pub unconstrained fn utility_context_at( self: Self, addr: AztecAddress, @@ -686,7 +946,8 @@ impl TestEnvironment { self.call_private_opts(from, CallPrivateOptions::new(), call) } - /// Variant of `call_private` which allows specifying multiple configuration values via `CallPrivateOptions`. + /// Variant of [`TestEnvironment::call_private`] which allows specifying multiple configuration values via + /// [`CallPrivateOptions`], including additional account scopes, authorized utility call targets, and gas settings. pub unconstrained fn call_private_opts( _self: Self, from: AztecAddress, @@ -705,6 +966,7 @@ impl TestEnvironment { /*is_static=*/ false, opts.additional_scopes, opts.authorized_utility_call_targets, + opts.resolve_gas_settings(), ); T::deserialize(serialized_return_values) @@ -747,6 +1009,13 @@ impl TestEnvironment { /*is_static=*/ true, opts.additional_scopes, opts.authorized_utility_call_targets, + // Views never produce a tx, so gas settings don't influence behaviour - we pass defaults. + resolve_gas_settings( + Option::none(), + Option::none(), + Option::none(), + Option::none(), + ), ); T::deserialize(serialized_return_values) @@ -868,6 +1137,19 @@ impl TestEnvironment { from: AztecAddress, call: PublicCall, ) -> T + where + T: Deserialize, + { + _self.call_public_opts(from, CallPublicOptions::new(), call) + } + + /// Variant of [`TestEnvironment::call_public`] which allows specifying gas settings via [`CallPublicOptions`]. + pub unconstrained fn call_public_opts( + _self: Self, + from: AztecAddress, + opts: CallPublicOptions, + call: PublicCall, + ) -> T where T: Deserialize, { @@ -877,6 +1159,7 @@ impl TestEnvironment { call.selector, call.args, false, + opts.resolve_gas_settings(), ); T::deserialize(serialized_return_values) } @@ -894,8 +1177,14 @@ impl TestEnvironment { call.selector, call.args, false, + // Incognito calls bypass the standard sender pathway; gas defaults are fine here. + resolve_gas_settings( + Option::none(), + Option::none(), + Option::none(), + Option::none(), + ), ); - T::deserialize(serialized_return_values) } @@ -914,6 +1203,13 @@ impl TestEnvironment { call.selector, call.args, true, + // Views never produce a tx, so gas settings don't influence behaviour - we pass defaults. + resolve_gas_settings( + Option::none(), + Option::none(), + Option::none(), + Option::none(), + ), ); T::deserialize(serialized_return_values) diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test.nr index 93328c346dd2..d45456950b0b 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test.nr @@ -1,4 +1,5 @@ mod accounts; +mod call_gas_settings; mod deployment; mod events; mod private_context; diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/call_gas_settings.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/call_gas_settings.nr new file mode 100644 index 000000000000..f954692e0152 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/call_gas_settings.nr @@ -0,0 +1,91 @@ +use crate::context::{PrivateCall, PublicCall}; +use crate::protocol::abis::{function_selector::FunctionSelector, gas::Gas, gas_settings::GasSettings}; +use crate::protocol::address::AztecAddress; +use crate::protocol::constants::{MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS}; +use crate::test::helpers::test_environment::{CallPrivateOptions, CallPublicOptions, TestEnvironment}; + +/// Path to the GasSettingsReader contract artifact in the noir-contracts workspace. Defined as a global because we +/// reference it from multiple tests; manual call construction (instead of importing the contract type and using +/// `GasSettingsReader::at(...)`) avoids the circular dependency that would otherwise arise between aztec-nr and a +/// contract that depends on aztec-nr. +global READER_PATH: str<65> = "../noir-contracts/@gas_settings_reader_contract/GasSettingsReader"; + +#[test] +unconstrained fn call_private_opts_propagates_gas_settings() { + let mut env = TestEnvironment::new(); + let caller = env.create_light_account(); + let contract_addr = env.deploy(READER_PATH).without_initializer(); + + // Note: we leave `max_fees_per_gas` and `max_priority_fees_per_gas` at their default zero values rather than + // exercise them here. Non-zero fee fields trigger a fee charge on the caller during tx execution, but the + // light-account caller has no fee juice balance. Asserting propagation of the fee fields through the call flow + // would require funding the caller. + let gas_limits = Gas::new(123_456, 789_012); + let teardown_gas_limits = Gas::new(1_111, 2_222); + + let call = PrivateCall::<_, _, GasSettings>::new( + contract_addr, + comptime { FunctionSelector::from_signature("read_gas_settings()") }, + "read_gas_settings", + [], + ); + + let returned = env.call_private_opts( + caller, + CallPrivateOptions::new().with_gas_limits(gas_limits).with_teardown_gas_limits(teardown_gas_limits), + call, + ); + + assert_eq(returned.gas_limits, gas_limits); + assert_eq(returned.teardown_gas_limits, teardown_gas_limits); +} + +/// Builds a `PublicCall` for `GasSettingsReader::read_public_gas_left`. +unconstrained fn read_public_gas_left_call(contract_addr: AztecAddress) -> PublicCall<20, 0, (u32, u32)> { + PublicCall::<_, _, (u32, u32)>::new( + contract_addr, + comptime { FunctionSelector::from_signature("read_public_gas_left()") }, + "read_public_gas_left", + [], + ) +} + +#[test] +unconstrained fn call_public_opts_propagates_l2_gas_limit() { + let mut env = TestEnvironment::new(); + let caller = env.create_light_account(); + let contract_addr = env.deploy(READER_PATH).without_initializer(); + + let l2_gas_limit = 5_000_000; + + let (_, l2_gas_left) = env.call_public_opts( + caller, + CallPublicOptions::new().with_gas_limits(Gas::new(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, l2_gas_limit)), + read_public_gas_left_call(contract_addr), + ); + + // The contract observes its remaining L2 gas after the AVM has charged for entering the function. That value must + // sit strictly below the configured limit (the function must have spent at least one gas unit) and above zero. + assert(l2_gas_left < l2_gas_limit); + assert(l2_gas_left > 0); +} + +#[test] +unconstrained fn call_public_opts_propagates_da_gas_limit() { + let mut env = TestEnvironment::new(); + let caller = env.create_light_account(); + let contract_addr = env.deploy(READER_PATH).without_initializer(); + + let da_gas_limit = 200_000; + + let (da_gas_left, _) = env.call_public_opts( + caller, + CallPublicOptions::new().with_gas_limits(Gas::new(da_gas_limit, MAX_PROCESSABLE_L2_GAS)), + read_public_gas_left_call(contract_addr), + ); + + // The function consumes zero DA gas (it only reads a counter), so we only verify the configured limit was + // honoured (`<= da_gas_limit`). The default `MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT` is much larger than + // `da_gas_limit`, so this still proves the configured value flowed through. + assert(da_gas_left <= da_gas_limit); +} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/misc.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/misc.nr index 78b8ecf93bcb..7ef08ae1b2cf 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/misc.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/misc.nr @@ -1,6 +1,9 @@ use crate::{ oracle::version::{ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR}, - test::helpers::test_environment::TestEnvironment, + test::helpers::{ + test_environment::TestEnvironment, + txe_oracles::{TXE_ORACLE_VERSION_MAJOR, TXE_ORACLE_VERSION_MINOR}, + }, }; use std::test::OracleMock; @@ -49,3 +52,16 @@ unconstrained fn oracle_version_is_checked_upon_env_creation() { assert_eq(mock.times_called(), 1); assert_eq(mock.get_last_params::<(Field, Field)>(), (ORACLE_VERSION_MAJOR, ORACLE_VERSION_MINOR)); } + +#[test] +unconstrained fn txe_oracle_version_is_checked_upon_env_creation() { + let mock = OracleMock::mock("aztec_txe_assertCompatibleOracleVersion"); + + let _env = TestEnvironment::new(); + + assert_eq(mock.times_called(), 1); + assert_eq( + mock.get_last_params::<(Field, Field)>(), + (TXE_ORACLE_VERSION_MAJOR, TXE_ORACLE_VERSION_MINOR), + ); +} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/private_context.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/private_context.nr index 5d31ca1768a3..0feac8ded02d 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/private_context.nr @@ -1,5 +1,10 @@ use crate::oracle::nullifiers::check_nullifier_exists; -use crate::protocol::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::FromField}; +use crate::protocol::{ + abis::{function_selector::FunctionSelector, gas::Gas, gas_fees::GasFees, gas_settings::GasSettings}, + address::AztecAddress, + constants::{MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS}, + traits::FromField, +}; use crate::test::helpers::test_environment::{PrivateContextOptions, TestEnvironment}; #[test] @@ -36,6 +41,74 @@ unconstrained fn opts_sets_contract_address_and_anchor_block_number() { ); } +#[test] +unconstrained fn default_gas_settings_match_protocol_max() { + let env = TestEnvironment::new(); + + env.private_context(|context| { + let gas_settings = context.gas_settings(); + assert_eq( + gas_settings.gas_limits, + Gas::new( + MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, + MAX_PROCESSABLE_L2_GAS, + ), + ); + assert_eq( + gas_settings.teardown_gas_limits, + Gas::new( + MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT / 8, + MAX_PROCESSABLE_L2_GAS / 8, + ), + ); + assert_eq(gas_settings.max_fees_per_gas, GasFees::new(0, 0)); + assert_eq(gas_settings.max_priority_fees_per_gas, GasFees::new(0, 0)); + }); +} + +#[test] +unconstrained fn opts_sets_individual_gas_fields() { + let env = TestEnvironment::new(); + + let gas_limits = Gas::new(123_456, 789_012); + let teardown_gas_limits = Gas::new(1_111, 2_222); + let max_fees_per_gas = GasFees::new(7, 11); + let max_priority_fees_per_gas = GasFees::new(3, 5); + + env.private_context_opts(PrivateContextOptions::new().with_gas_limits(gas_limits), |context| { + assert_eq(context.gas_settings().gas_limits, gas_limits); + }); + + env.private_context_opts(PrivateContextOptions::new().with_teardown_gas_limits(teardown_gas_limits), |context| { + assert_eq(context.gas_settings().teardown_gas_limits, teardown_gas_limits); + }); + + env.private_context_opts(PrivateContextOptions::new().with_max_fees_per_gas(max_fees_per_gas), |context| { + assert_eq(context.gas_settings().max_fees_per_gas, max_fees_per_gas); + }); + + env.private_context_opts( + PrivateContextOptions::new().with_max_priority_fees_per_gas(max_priority_fees_per_gas), + |context| { assert_eq(context.gas_settings().max_priority_fees_per_gas, max_priority_fees_per_gas); }, + ); +} + +#[test] +unconstrained fn opts_with_gas_settings_sets_all_fields() { + let env = TestEnvironment::new(); + + let gas_settings = GasSettings::new( + Gas::new(123_456, 789_012), + Gas::new(1_111, 2_222), + GasFees::new(7, 11), + GasFees::new(3, 5), + ); + + env.private_context_opts(PrivateContextOptions::new().with_gas_settings(gas_settings), |context| { + assert_eq(context.gas_settings(), gas_settings); + }); +} + #[test] unconstrained fn uses_last_block_number() { let env = TestEnvironment::new(); diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr index ed82665af037..7c783f8b37a0 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr @@ -4,16 +4,32 @@ use crate::{ }; use crate::protocol::{ - abis::function_selector::FunctionSelector, + abis::{function_selector::FunctionSelector, gas_settings::GasSettings}, address::AztecAddress, constants::{ - CONTRACT_INSTANCE_LENGTH, MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX, MAX_PRIVATE_LOGS_PER_TX, - PRIVATE_LOG_SIZE_IN_FIELDS, + CONTRACT_INSTANCE_LENGTH, GAS_SETTINGS_LENGTH, MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX, + MAX_PRIVATE_LOGS_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS, }, contract_instance::ContractInstance, - traits::{Deserialize, ToField}, + traits::{Deserialize, Serialize, ToField}, }; +/// The TXE oracle version constants are used to check that the oracle interface used for tests is in sync between +/// TXE and Aztec.nr. This is separate from the contract oracle version in +/// [`oracle::version`](crate::oracle::version), which covers oracles used during contract execution by PXE. +/// +/// The TypeScript counterparts are in `yarn-project/txe/src/txe_oracle_version.ts`. +pub global TXE_ORACLE_VERSION_MAJOR: Field = 1; +pub global TXE_ORACLE_VERSION_MINOR: Field = 0; + +/// Asserts that the TXE oracle interface version is compatible. +pub unconstrained fn assert_compatible_txe_oracle_version() { + assert_compatible_txe_oracle_version_oracle_call(TXE_ORACLE_VERSION_MAJOR, TXE_ORACLE_VERSION_MINOR); +} + +#[oracle(aztec_txe_assertCompatibleOracleVersion)] +unconstrained fn assert_compatible_txe_oracle_version_oracle_call(major: Field, minor: Field) {} + /// Upper bound on the number of raw offchain effects the TXE test environment will surface /// to Noir in a single `get_last_call_offchain_effects` query. pub(crate) global MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY: u32 = 64; @@ -45,6 +61,7 @@ pub unconstrained fn private_call_new_flow [Field; N] { private_call_new_flow_oracle( from, @@ -55,6 +72,7 @@ pub unconstrained fn private_call_new_flow( function_selector: FunctionSelector, args: [Field; M], is_static_call: bool, + gas_settings: GasSettings, ) -> [Field; N] { let calldata = [function_selector.to_field()].concat(args); - public_call_new_flow_oracle(from, contract_address, calldata, is_static_call) + public_call_new_flow_oracle( + from, + contract_address, + calldata, + is_static_call, + gas_settings.serialize(), + ) +} + +pub unconstrained fn set_private_txe_context( + contract_address: Option, + anchor_block_number: Option, + gas_settings: GasSettings, +) -> PrivateContextInputs { + set_private_txe_context_oracle( + contract_address, + anchor_block_number, + gas_settings.serialize(), + ) } pub unconstrained fn execute_utility_function( @@ -226,6 +263,7 @@ unconstrained fn private_call_new_flow_oracle [Field; N] {} #[oracle(aztec_txe_publicCallNewFlow)] @@ -234,6 +272,7 @@ unconstrained fn public_call_new_flow_oracle( contract_address: AztecAddress, calldata: [Field; M], is_static_call: bool, + gas_settings: [Field; GAS_SETTINGS_LENGTH], ) -> [Field; N] {} #[oracle(aztec_txe_executeUtilityFunction)] @@ -248,9 +287,10 @@ unconstrained fn execute_utility_function_oracle, - anchor_block_number: Option, +unconstrained fn set_private_txe_context_oracle( + _contract_address: Option, + _anchor_block_number: Option, + _gas_settings: [Field; GAS_SETTINGS_LENGTH], ) -> PrivateContextInputs {} #[oracle(aztec_txe_setPublicTXEContext)] diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index a47fa19d16ec..d0a2168911fe 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -35,6 +35,7 @@ members = [ "contracts/protocol/public_checks_contract", "contracts/protocol_interface/contract_instance_registry_interface", "contracts/protocol_interface/fee_juice_interface", + "contracts/test/gas_settings_reader_contract", "contracts/test/generic_proxy_contract", "contracts/test/abi_types_contract", "contracts/test/auth_wit_test_contract", diff --git a/noir-projects/noir-contracts/contracts/test/gas_settings_reader_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/test/gas_settings_reader_contract/Nargo.toml new file mode 100644 index 000000000000..6a3591efec75 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/gas_settings_reader_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "gas_settings_reader_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/test/gas_settings_reader_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/gas_settings_reader_contract/src/main.nr new file mode 100644 index 000000000000..39826a71695c --- /dev/null +++ b/noir-projects/noir-contracts/contracts/test/gas_settings_reader_contract/src/main.nr @@ -0,0 +1,21 @@ +use aztec::macros::aztec; + +/// Used by aztec-nr's test_environment gas-settings tests to verify that values configured using `TestEnvironment` +/// reach the called function's context. +#[aztec] +pub contract GasSettingsReader { + use aztec::{macros::functions::external, protocol::abis::gas_settings::GasSettings}; + + /// Returns the gas settings observed by the called private function. + #[external("private")] + fn read_gas_settings() -> GasSettings { + self.context.gas_settings() + } + + /// Returns the remaining `(da, l2)` gas observed by the called public function. Together with + /// the function's own gas usage these reveal the configured `gas_limits`. + #[external("public")] + fn read_public_gas_left() -> (u32, u32) { + (self.context.da_gas_left(), self.context.l2_gas_left()) + } +} diff --git a/noir-projects/noir-contracts/contracts/test/nested_utility_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/nested_utility_contract/src/main.nr index 331a14084390..c212d6dbecae 100644 --- a/noir-projects/noir-contracts/contracts/test/nested_utility_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/nested_utility_contract/src/main.nr @@ -8,13 +8,13 @@ mod test; #[aztec] pub contract NestedUtility { + use crate::pow_note::PowNote; use aztec::macros::{functions::external, storage::storage}; use aztec::{ messages::message_delivery::MessageDelivery, protocol::address::AztecAddress, state_vars::{Owned, PrivateMutable}, }; - use crate::pow_note::PowNote; #[storage] struct Storage { @@ -53,10 +53,7 @@ pub contract NestedUtility { /// Cross-contract version: calls pow_from_storage on the target contract. #[external("utility")] - unconstrained fn delegate_pow_from_storage( - target: AztecAddress, - owner: AztecAddress, - ) -> Field { + unconstrained fn delegate_pow_from_storage(target: AztecAddress, owner: AztecAddress) -> Field { self.call(NestedUtility::at(target).pow_from_storage(owner)) } diff --git a/yarn-project/pxe/src/bin/check_oracle_version.ts b/yarn-project/pxe/src/bin/check_oracle_version.ts index 31419452447d..1393d7df640e 100644 --- a/yarn-project/pxe/src/bin/check_oracle_version.ts +++ b/yarn-project/pxe/src/bin/check_oracle_version.ts @@ -17,7 +17,7 @@ import { ORACLE_INTERFACE_HASH } from '../oracle_version.js'; * - If the change is backward-breaking (e.g. removing/renaming an oracle), bump ORACLE_VERSION_MAJOR. * - If the change is an oracle addition (non-breaking), bump ORACLE_VERSION_MINOR. * - * TODO(#16581): The following only takes into consideration changes to the oracles defined in Oracle.ts and omits TXE + * TODO(F-667): The following only takes into consideration changes to the oracles defined in Oracle.ts and omits TXE * oracles. Ensure this checks TXE oracles as well. This hasn't been implemented yet since we don't have a clean TXE * oracle interface like we do in PXE (i.e., there is no single Oracle class that contains only the oracles). */ diff --git a/yarn-project/txe/src/oracle/interfaces.ts b/yarn-project/txe/src/oracle/interfaces.ts index 7aaf8e94c95a..0d0ecdc845cc 100644 --- a/yarn-project/txe/src/oracle/interfaces.ts +++ b/yarn-project/txe/src/oracle/interfaces.ts @@ -6,6 +6,7 @@ import { BlockNumber } from '@aztec/foundation/branded-types'; import type { Fr } from '@aztec/foundation/curves/bn254'; import type { EventSelector, FunctionSelector } from '@aztec/stdlib/abi'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; +import type { GasSettings } from '@aztec/stdlib/gas'; import type { PrivateLog } from '@aztec/stdlib/logs'; import type { UInt64 } from '@aztec/stdlib/types'; @@ -72,6 +73,7 @@ export interface ITxeExecutionOracle { additionalScopes: AztecAddress[], jobId: string, authorizedUtilityCallTargets: AztecAddress[], + gasSettings: GasSettings, ): Promise<{ returnValues: Fr[]; offchainEffects: Fr[][] }>; executeUtilityFunction( targetContractAddress: AztecAddress, @@ -85,6 +87,7 @@ export interface ITxeExecutionOracle { targetContractAddress: AztecAddress, calldata: Fr[], isStaticCall: boolean, + gasSettings: GasSettings, ): Promise; // TODO(F-335): Drop this from here as it's not a real oracle handler - it's only called from // RPCTranslator::txeGetPrivateEvents and never from Noir. diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 5fc1bea228bf..b4908a7663f9 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -1,9 +1,4 @@ -import { - CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, - MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, - MAX_PROCESSABLE_L2_GAS, - NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, -} from '@aztec/constants'; +import { CONTRACT_INSTANCE_REGISTRY_CONTRACT_ADDRESS, NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP } from '@aztec/constants'; import { BlockNumber } from '@aztec/foundation/branded-types'; import { Schnorr } from '@aztec/foundation/crypto/schnorr'; import { Fr } from '@aztec/foundation/curves/bn254'; @@ -56,13 +51,7 @@ import { AuthWitness } from '@aztec/stdlib/auth-witness'; import { PublicSimulatorConfig } from '@aztec/stdlib/avm'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { type ContractInstanceWithAddress, computePartialAddress } from '@aztec/stdlib/contract'; -import { - FALLBACK_TEARDOWN_DA_GAS_LIMIT, - FALLBACK_TEARDOWN_L2_GAS_LIMIT, - Gas, - GasFees, - GasSettings, -} from '@aztec/stdlib/gas'; +import { Gas, GasFees, GasSettings } from '@aztec/stdlib/gas'; import { computeCalldataHash, computeProtocolNullifier, siloNullifier } from '@aztec/stdlib/hash'; import { PartialPrivateTailPublicInputsForPublic, @@ -329,6 +318,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl additionalScopes: AztecAddress[] = [], jobId: string, authorizedUtilityCallTargets: AztecAddress[], + gasSettings: GasSettings, ) { this.logger.verbose( `Executing external function ${await this.contractStore.getDebugFunctionName(targetContractAddress, functionSelector)}@${targetContractAddress} isStaticCall=${isStaticCall}`, @@ -364,10 +354,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl const msgSender = from ?? AztecAddress.NULL_MSG_SENDER; const callContext = new CallContext(msgSender, targetContractAddress, functionSelector, isStaticCall); - const gasLimits = new Gas(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS); - const teardownGasLimits = new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, FALLBACK_TEARDOWN_L2_GAS_LIMIT); - const gasSettings = new GasSettings(gasLimits, teardownGasLimits, GasFees.empty(), GasFees.empty()); - const txContext = new TxContext(this.chainId, this.version, gasSettings); const protocolNullifier = await computeProtocolNullifier(getSingleTxBlockRequestHash(blockNumber)); @@ -563,6 +549,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl targetContractAddress: AztecAddress, calldata: Fr[], isStaticCall: boolean, + gasSettings: GasSettings, ) { this.logger.verbose( `Executing public function ${await this.contractStore.getDebugFunctionName(targetContractAddress, FunctionSelector.fromField(calldata[0]))}@${targetContractAddress} isStaticCall=${isStaticCall}`, @@ -570,12 +557,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl const blockNumber = await this.getNextBlockNumber(); - const gasLimits = new Gas(MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT, MAX_PROCESSABLE_L2_GAS); - - const teardownGasLimits = new Gas(FALLBACK_TEARDOWN_DA_GAS_LIMIT, FALLBACK_TEARDOWN_L2_GAS_LIMIT); - - const gasSettings = new GasSettings(gasLimits, teardownGasLimits, GasFees.empty(), GasFees.empty()); - const txContext = new TxContext(this.chainId, this.version, gasSettings); const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader(); diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index 78fb5f2c3545..6d9e2882544a 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -19,8 +19,10 @@ import { import { type ContractArtifact, EventSelector, FunctionSelector, NoteSelector } from '@aztec/stdlib/abi'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { BlockHash } from '@aztec/stdlib/block'; +import { GasSettings } from '@aztec/stdlib/gas'; import type { IAvmExecutionOracle, ITxeExecutionOracle } from './oracle/interfaces.js'; +import { TXE_ORACLE_VERSION_MAJOR } from './txe_oracle_version.js'; import type { TXESessionStateHandler } from './txe_session.js'; import { type ForeignCallArray, @@ -111,6 +113,26 @@ export class RPCTranslator { return this.oracleHandler; } + // eslint-disable-next-line camelcase + aztec_txe_assertCompatibleOracleVersion(foreignMajor: ForeignCallSingle, foreignMinor: ForeignCallSingle) { + const major = fromSingle(foreignMajor).toNumber(); + const minor = fromSingle(foreignMinor).toNumber(); + + if (major !== TXE_ORACLE_VERSION_MAJOR) { + const hint = + major > TXE_ORACLE_VERSION_MAJOR + ? 'The test was compiled with a newer version of Aztec.nr than your test environment supports. Upgrade your test environment to a compatible version.' + : 'The test was compiled with an older version of Aztec.nr than your test environment supports. Recompile the test with a compatible version of Aztec.nr.'; + throw new Error( + `Incompatible test environment version: ${hint} See https://docs.aztec.network/errors/12 (expected test oracle major version ${TXE_ORACLE_VERSION_MAJOR}, got ${major})`, + ); + } + + this.stateHandler.setTxeOracleVersion({ major, minor }); + + return toForeignCallResult([]); + } + // TXE session state transition functions - these get handled by the state handler // eslint-disable-next-line camelcase @@ -126,6 +148,7 @@ export class RPCTranslator { foreignContractAddressValue: ForeignCallSingle, foreignAnchorBlockNumberIsSome: ForeignCallSingle, foreignAnchorBlockNumberValue: ForeignCallSingle, + foreignGasSettings: ForeignCallArray, ) { const contractAddress = fromSingle(foreignContractAddressIsSome).toBool() ? AztecAddress.fromField(fromSingle(foreignContractAddressValue)) @@ -135,7 +158,13 @@ export class RPCTranslator { ? BlockNumber(fromSingle(foreignAnchorBlockNumberValue).toNumber()) : undefined; - const privateContextInputs = await this.stateHandler.enterPrivateState(contractAddress, anchorBlockNumber); + const gasSettings = GasSettings.fromFields(fromArray(foreignGasSettings)); + + const privateContextInputs = await this.stateHandler.enterPrivateState( + contractAddress, + anchorBlockNumber, + gasSettings, + ); return toForeignCallResult(privateContextInputs.toFields().map(toSingle)); } @@ -1371,6 +1400,7 @@ export class RPCTranslator { foreignIsStaticCall: ForeignCallSingle, foreignAdditionalScopes: ForeignCallArray, foreignAuthorizedUtilityCallTargets: ForeignCallArray, + foreignGasSettings: ForeignCallArray, ) { const from = fromSingle(foreignFromIsSome).toBool() ? addressFromSingle(foreignFromValue) : undefined; const targetContractAddress = addressFromSingle(foreignTargetContractAddress); @@ -1382,6 +1412,7 @@ export class RPCTranslator { const authorizedUtilityCallTargets = fromArray(foreignAuthorizedUtilityCallTargets).map(field => AztecAddress.fromField(field), ); + const gasSettings = GasSettings.fromFields(fromArray(foreignGasSettings)); const returnValues = await this.stateHandler.withTopLevelCallTracking(async () => { const { returnValues, offchainEffects } = await this.handlerAsTxe().privateCallNewFlow( @@ -1394,6 +1425,7 @@ export class RPCTranslator { additionalScopes, this.stateHandler.getCurrentJob(), authorizedUtilityCallTargets, + gasSettings, ); // Private execution collects offchain effects inside PXE's PrivateExecutionOracle rather than @@ -1459,14 +1491,22 @@ export class RPCTranslator { foreignAddress: ForeignCallSingle, foreignCalldata: ForeignCallArray, foreignIsStaticCall: ForeignCallSingle, + foreignGasSettings: ForeignCallArray, ) { const from = fromSingle(foreignFromIsSome).toBool() ? addressFromSingle(foreignFromValue) : undefined; const address = addressFromSingle(foreignAddress); const calldata = fromArray(foreignCalldata); const isStaticCall = fromSingle(foreignIsStaticCall).toBool(); + const gasSettings = GasSettings.fromFields(fromArray(foreignGasSettings)); const returnValues = await this.stateHandler.withTopLevelCallTracking(async () => { - const returnValues = await this.handlerAsTxe().publicCallNewFlow(from, address, calldata, isStaticCall); + const returnValues = await this.handlerAsTxe().publicCallNewFlow( + from, + address, + calldata, + isStaticCall, + gasSettings, + ); // TODO(F-335): Avoid doing the following call here. await this.stateHandler.cycleJob(); diff --git a/yarn-project/txe/src/txe_oracle_version.ts b/yarn-project/txe/src/txe_oracle_version.ts new file mode 100644 index 000000000000..ae8712abb4c0 --- /dev/null +++ b/yarn-project/txe/src/txe_oracle_version.ts @@ -0,0 +1,9 @@ +/** + * The TXE oracle version constants are used to check that the oracle interface used for tests is in sync between + * TXE and Aztec.nr. This is separate from the contract oracle version in `pxe/src/oracle_version.ts`, which covers + * oracles used during contract execution by PXE. + * + * The Noir counterparts are in `noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr`. + */ +export const TXE_ORACLE_VERSION_MAJOR = 1; +export const TXE_ORACLE_VERSION_MINOR = 0; diff --git a/yarn-project/txe/src/txe_session.test.ts b/yarn-project/txe/src/txe_session.test.ts index 6422a2cd8355..3dd5676b9ea2 100644 --- a/yarn-project/txe/src/txe_session.test.ts +++ b/yarn-project/txe/src/txe_session.test.ts @@ -32,26 +32,20 @@ describe('TXESession.processFunction', () => { it('rejects calling a function that does not exist on RPCTranslator with the expected error message', () => { const invalidName = 'notARealFunction' as unknown as TXEOracleFunctionName; - expect(() => session.processFunction(invalidName, [])).toThrow( - `notARealFunction does not correspond to any oracle handler available on RPCTranslator`, - ); + expect(() => session.processFunction(invalidName, [])).toThrow(`Unknown oracle 'notARealFunction'.`); }); it('rejects calling internal translator helpers (handlerAs*) with the expected error message', () => { const illegalNames = ['handlerAsMisc', 'handlerAsUtility', 'handlerAsPrivate', 'handlerAsAvm', 'handlerAsTxe']; for (const name of illegalNames) { - expect(() => session.processFunction(name as any, [])).toThrow( - `${name} does not correspond to any oracle handler available on RPCTranslator`, - ); + expect(() => session.processFunction(name as any, [])).toThrow(`Unknown oracle '${name}'.`); } }); it("rejects calling the translator's constructor with the expected error message", () => { const invalidName = 'constructor' as unknown as TXEOracleFunctionName; - expect(() => session.processFunction(invalidName, [])).toThrow( - `constructor does not correspond to any oracle handler available on RPCTranslator`, - ); + expect(() => session.processFunction(invalidName, [])).toThrow(`Unknown oracle 'constructor'.`); }); }); diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index a075621ab6d6..2c7c7fd493fb 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -38,7 +38,7 @@ import { import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi'; import type { AuthWitness } from '@aztec/stdlib/auth-witness'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { GasSettings } from '@aztec/stdlib/gas'; +import type { GasSettings } from '@aztec/stdlib/gas'; import { computeProtocolNullifier } from '@aztec/stdlib/hash'; import { PrivateContextInputs } from '@aztec/stdlib/kernel'; import { makeGlobalVariables } from '@aztec/stdlib/testing'; @@ -53,6 +53,7 @@ import { TXEOracleTopLevelContext } from './oracle/txe_oracle_top_level_context. import { RPCTranslator } from './rpc_translator.js'; import { TXEArchiver } from './state_machine/archiver.js'; import { TXEStateMachine } from './state_machine/index.js'; +import { TXE_ORACLE_VERSION_MAJOR, TXE_ORACLE_VERSION_MINOR } from './txe_oracle_version.js'; import type { ForeignCallArgs, ForeignCallResult } from './util/encoding.js'; import { TXEAccountStore } from './util/txe_account_store.js'; import { getSingleTxBlockRequestHash, insertTxEffectIntoWorldTrees, makeTXEBlock } from './utils/block_creation.js'; @@ -109,9 +110,16 @@ export type TXEOracleFunctionName = Exclude< >; export interface TXESessionStateHandler { + /** Records the TXE oracle version reported by the Noir test code for diagnostics. */ + setTxeOracleVersion(version: { major: number; minor: number }): void; + enterTopLevelState(): Promise; enterPublicState(contractAddress?: AztecAddress): Promise; - enterPrivateState(contractAddress?: AztecAddress, anchorBlockNumber?: BlockNumber): Promise; + enterPrivateState( + contractAddress: AztecAddress | undefined, + anchorBlockNumber: BlockNumber | undefined, + gasSettings: GasSettings, + ): Promise; enterUtilityState(contractAddress?: AztecAddress): Promise; // TODO(F-335): Exposing the job info is abstraction breakage - drop the following 2 functions. @@ -186,6 +194,7 @@ export class TXESession implements TXESessionStateHandler { private state: SessionState = { name: 'TOP_LEVEL' }; private authwits: Map = new Map(); private lastCallInfo: LastCallState = emptyLastCallState(); + private txeOracleVersion: { major: number; minor: number } | undefined; constructor( private logger: Logger, @@ -306,7 +315,28 @@ export class TXESession implements TXESessionStateHandler { return translator[validatedFunctionName](...inputs); } catch (error) { if (error instanceof z.ZodError) { - throw new Error(`${functionName} does not correspond to any oracle handler available on RPCTranslator`); + let versionHint: string; + if (!this.txeOracleVersion) { + versionHint = + ' The test appears to use an older version of Aztec.nr that does not' + + ' support test environment oracle versioning. Update Aztec.nr to a compatible version.' + + ' See https://docs.aztec.network/errors/12'; + } else if (this.txeOracleVersion.minor > TXE_ORACLE_VERSION_MINOR) { + versionHint = + ` The test uses Aztec.nr test oracle version` + + ` ${this.txeOracleVersion.major}.${this.txeOracleVersion.minor}, but this test environment` + + ` only supports up to ${TXE_ORACLE_VERSION_MAJOR}.${TXE_ORACLE_VERSION_MINOR}.` + + ` Upgrade the Aztec CLI to a compatible version.` + + ` See https://docs.aztec.network/errors/12`; + } else { + versionHint = + ` The test's oracle version (${this.txeOracleVersion.major}.${this.txeOracleVersion.minor})` + + ` is compatible with this test environment` + + ` (${TXE_ORACLE_VERSION_MAJOR}.${TXE_ORACLE_VERSION_MINOR}), so this oracle should be` + + ` available. This is an unexpected error, please report it.` + + ` See https://docs.aztec.network/errors/13`; + } + throw new Error(`Unknown oracle '${functionName}'.${versionHint}`); } else if (error instanceof Error) { throw new Error( `Execution error while processing function ${functionName} in state ${this.state.name}: ${error.message}`, @@ -375,6 +405,11 @@ export class TXESession implements TXESessionStateHandler { return { txHash, anchorBlockTimestamp }; } + setTxeOracleVersion(version: { major: number; minor: number }): void { + this.txeOracleVersion = version; + this.logger.debug(`Test compiled with test oracle version ${version.major}.${version.minor}`); + } + async enterTopLevelState() { switch (this.state.name) { case 'PRIVATE': { @@ -424,7 +459,8 @@ export class TXESession implements TXESessionStateHandler { async enterPrivateState( contractAddress: AztecAddress = DEFAULT_ADDRESS, - anchorBlockNumber?: BlockNumber, + anchorBlockNumber: BlockNumber | undefined, + gasSettings: GasSettings, ): Promise { this.exitTopLevelState(); this.resetLastCall(); @@ -455,7 +491,7 @@ export class TXESession implements TXESessionStateHandler { const utilityExecutor = this.utilityExecutorForContractSync(anchorBlock); this.oracleHandler = new PrivateExecutionOracle({ argsHash: Fr.ZERO, - txContext: new TxContext(this.chainId, this.version, GasSettings.empty()), + txContext: new TxContext(this.chainId, this.version, gasSettings), callContext: new CallContext(AztecAddress.ZERO, contractAddress, FunctionSelector.empty(), false), anchorBlockHeader: anchorBlock!, utilityExecutor,