diff --git a/docs/docs/migration_notes.md b/docs/docs/migration_notes.md index 17bd8f536307..8739713909f9 100644 --- a/docs/docs/migration_notes.md +++ b/docs/docs/migration_notes.md @@ -8,6 +8,17 @@ Aztec is in full-speed development. Literally every version breaks compatibility ## 0.42.0 +### [Aztec.nr] Unconstrained Context + +Top-level unconstrained execution is now marked by the new `UnconstrainedContext`, which provides access to the block number and contract address being used in the simulation. Any custom state variables that provided unconstrained functions should update their specialization parameter: + +```diff ++ use dep::aztec::context::UnconstrainedContext; + +- impl MyStateVariable<()> { ++ impl MyStateVariable { +``` + ### [Aztec.nr] Filtering is now constrained The `filter` argument of `NoteGetterOptions` (typically passed via the `with_filter()` function) is now applied in a constraining environment, meaning any assertions made during the filtering are guaranteed to hold. This mirrors the behavior of the `select()` function. diff --git a/docs/docs/reference/smart_contract_reference/storage/index.md b/docs/docs/reference/smart_contract_reference/storage/index.md index f82ba54ae6d1..f0a436c2e66e 100644 --- a/docs/docs/reference/smart_contract_reference/storage/index.md +++ b/docs/docs/reference/smart_contract_reference/storage/index.md @@ -27,9 +27,10 @@ On this and the following pages in this section, you’ll learn: Aztec contracts have three different modes of execution: [private](../../../aztec/glossary/call_types.md#private-execution), [public](../../../aztec/glossary/call_types.md#public-execution) and [top-level unconstrained](../../../aztec/glossary/call_types.md#top-level-unconstrained). How storage is accessed depends on the execution mode: for example, `PublicImmutable` can be read in all execution modes but only initialized in public, while `PrivateMutable` is entirely unavailable in public. Aztec.nr prevents developers from calling functions unavailable in the current execution mode via the `context` variable that is injected into all contract functions. Its type indicates the current execution mode: - - `&mut PrivateContext` for private execution - - `&mut PublicContext` for public execution - - `()` for unconstrained + +- `&mut PrivateContext` for private execution +- `&mut PublicContext` for public execution +- `UncontrainedContext` for top-level unconstrained execution All state variables are generic over this `Context` type, and expose different methods in each execution mode. In the example above, `PublicImmutable`'s `initialize` function is only available with a public execution context, and so the following code results in a compilation error: diff --git a/noir-projects/aztec-nr/aztec/src/context.nr b/noir-projects/aztec-nr/aztec/src/context.nr index 4866291da006..8c973d70e1c5 100644 --- a/noir-projects/aztec-nr/aztec/src/context.nr +++ b/noir-projects/aztec-nr/aztec/src/context.nr @@ -4,6 +4,8 @@ mod inputs; mod packed_returns; mod private_context; mod public_context; +mod unconstrained_context; + mod call_interfaces; mod gas; @@ -16,3 +18,4 @@ use private_context::PrivateContext; use packed_returns::PackedReturns; use public_context::PublicContext; use public_context::FunctionReturns; +use unconstrained_context::UnconstrainedContext; diff --git a/noir-projects/aztec-nr/aztec/src/context/unconstrained_context.nr b/noir-projects/aztec-nr/aztec/src/context/unconstrained_context.nr new file mode 100644 index 000000000000..4af2e230f289 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/context/unconstrained_context.nr @@ -0,0 +1,32 @@ +use dep::protocol_types::address::AztecAddress; + +struct UnconstrainedContext { + block_number: u32, + contract_address: AztecAddress, +} + +impl UnconstrainedContext { + fn new() -> Self { + // We could call these oracles on the getters instead of at creation, which makes sense given that they might + // not even be accessed. However any performance gains are minimal, and we'd rather fail early if a user + // incorrectly attempts to create an UnconstrainedContext in an environment in which these oracles are not + // available. + let block_number = block_number_oracle(); + let contract_address = contract_address_oracle(); + Self { block_number, contract_address } + } + + fn block_number(self) -> u32 { + self.block_number + } + + fn contract_address(self) -> AztecAddress { + self.contract_address + } +} + +#[oracle(getContractAddress)] +fn contract_address_oracle() -> AztecAddress {} + +#[oracle(getBlockNumber)] +fn block_number_oracle() -> u32 {} diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr index 3c70465a8d8b..967df14e52f0 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_immutable.nr @@ -3,7 +3,7 @@ use dep::protocol_types::{ constants::GENERATOR_INDEX__INITIALIZATION_NULLIFIER, hash::pedersen_hash }; -use crate::context::PrivateContext; +use crate::context::{PrivateContext, UnconstrainedContext}; use crate::note::{ lifecycle::create_note, note_getter::{get_note, view_notes}, note_interface::NoteInterface, note_viewer_options::NoteViewerOptions @@ -66,7 +66,7 @@ impl PrivateImmutable { // docs:end:get_note } -impl PrivateImmutable { +impl PrivateImmutable { // docs:start:is_initialized unconstrained pub fn is_initialized(self) -> bool { let nullifier = self.compute_initialization_nullifier(); diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr index 9e7934c3bf1d..b7e37162a966 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable.nr @@ -3,7 +3,7 @@ use dep::protocol_types::{ grumpkin_point::GrumpkinPoint, hash::pedersen_hash }; -use crate::context::PrivateContext; +use crate::context::{PrivateContext, UnconstrainedContext}; use crate::note::{ lifecycle::{create_note, destroy_note}, note_getter::{get_note, view_notes}, note_interface::NoteInterface, note_viewer_options::NoteViewerOptions @@ -124,7 +124,7 @@ impl PrivateMutable { // docs:end:get_note } -impl PrivateMutable { +impl PrivateMutable { unconstrained pub fn is_initialized(self) -> bool { let nullifier = self.compute_initialization_nullifier(); check_nullifier_exists(nullifier) diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr index 3029df8bcdaf..0f844881db9a 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr @@ -2,7 +2,7 @@ use dep::protocol_types::{ constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, abis::read_request::ReadRequest, grumpkin_point::GrumpkinPoint }; -use crate::context::{PrivateContext, PublicContext}; +use crate::context::{PrivateContext, PublicContext, UnconstrainedContext}; use crate::note::{ constants::MAX_NOTES_PER_PAGE, lifecycle::{create_note, create_note_hash_from_public, destroy_note}, note_getter::{get_notes, view_notes}, note_getter_options::NoteGetterOptions, @@ -85,7 +85,7 @@ impl PrivateSet { // docs:end:get_notes } -impl PrivateSet { +impl PrivateSet { // docs:start:view_notes unconstrained pub fn view_notes( self, 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 5d9ad2b7e300..b8f2c1d2dde6 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 @@ -1,4 +1,7 @@ -use crate::{context::PublicContext, oracle::{storage::{storage_read, storage_write}}, state_vars::storage::Storage}; +use crate::{ + context::{PublicContext, UnconstrainedContext}, oracle::{storage::{storage_read, storage_write}}, + state_vars::storage::Storage +}; use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::{Deserialize, Serialize}}; // Just like SharedImmutable but without the ability to read from private functions. @@ -54,11 +57,11 @@ impl PublicImmutable { // docs:end:public_immutable_struct_read } -impl PublicImmutable { +impl PublicImmutable { pub fn read(self) -> T where T: Deserialize { - // Note that this is the exact same implementation as for public execution, though it might change in the future - // since unconstrained execution might not rely on the same oracles as used for public execution (which - // transpile to AVM opcodes). + // This looks the same as the &mut PublicContext impl, but is actually very different. In public execution the + // storage read oracle gets transpiled to SLOAD opcodes, whereas in unconstrained execution the PXE returns + // historical data. let fields = storage_read(self.storage_slot); T::deserialize(fields) } 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 f3ce3a6d1405..69a6a0f8a482 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 @@ -1,4 +1,4 @@ -use crate::context::PublicContext; +use crate::context::{PublicContext, UnconstrainedContext}; use crate::oracle::storage::storage_read; use crate::oracle::storage::storage_write; use dep::protocol_types::traits::{Deserialize, Serialize}; @@ -42,11 +42,11 @@ impl PublicMutable { // docs:end:public_mutable_struct_write } -impl PublicMutable { +impl PublicMutable { pub fn read(self) -> T where T: Deserialize { - // Note that this is the exact same implementation as for public execution, though it might change in the future - // since unconstrained execution might not rely on the same oracles as used for public execution (which - // transpile to AVM opcodes). + // This looks the same as the &mut PublicContext impl, but is actually very different. In public execution the + // storage read oracle gets transpiled to SLOAD opcodes, whereas in unconstrained execution the PXE returns + // historical data. let fields = storage_read(self.storage_slot); T::deserialize(fields) } diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr b/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr index 7662ec34d189..086c47aca56f 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/shared_immutable.nr @@ -1,6 +1,6 @@ use crate::{ - context::{PrivateContext, PublicContext}, oracle::{storage::{storage_read, storage_write}}, - state_vars::storage::Storage + context::{PrivateContext, PublicContext, UnconstrainedContext}, + oracle::{storage::{storage_read, storage_write}}, state_vars::storage::Storage }; use dep::protocol_types::{constants::INITIALIZATION_SLOT_SEPARATOR, traits::{Deserialize, Serialize}}; @@ -49,7 +49,7 @@ impl SharedImmutable { } } -impl SharedImmutable { +impl SharedImmutable { pub fn read_public(self) -> T where T: Deserialize { let fields = storage_read(self.storage_slot); T::deserialize(fields) diff --git a/noir-projects/aztec-nr/value-note/src/balance_utils.nr b/noir-projects/aztec-nr/value-note/src/balance_utils.nr index 5cbe5a4d9d6d..35b35d7e2dda 100644 --- a/noir-projects/aztec-nr/value-note/src/balance_utils.nr +++ b/noir-projects/aztec-nr/value-note/src/balance_utils.nr @@ -1,12 +1,14 @@ -use dep::aztec::note::{note_getter::view_notes, note_viewer_options::NoteViewerOptions}; -use dep::aztec::state_vars::PrivateSet; +use dep::aztec::{ + context::UnconstrainedContext, state_vars::PrivateSet, + note::{note_getter::view_notes, note_viewer_options::NoteViewerOptions} +}; use crate::value_note::ValueNote; -unconstrained pub fn get_balance(set: PrivateSet) -> Field { +unconstrained pub fn get_balance(set: PrivateSet) -> Field { get_balance_with_offset(set, 0) } -unconstrained pub fn get_balance_with_offset(set: PrivateSet, offset: u32) -> Field { +unconstrained pub fn get_balance_with_offset(set: PrivateSet, offset: u32) -> Field { let mut balance = 0; // docs:start:view_notes let mut options = NoteViewerOptions::new(); diff --git a/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr b/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr index 0f4daf039e6e..d10ca9e1739f 100644 --- a/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr +++ b/noir-projects/noir-contracts/contracts/card_game_contract/src/cards.nr @@ -1,6 +1,7 @@ use dep::aztec::prelude::{AztecAddress, FunctionSelector, PrivateContext, NoteHeader, NoteGetterOptions, NoteViewerOptions}; use dep::aztec::{ + context::UnconstrainedContext, protocol_types::{ traits::{ToField, Serialize, FromField}, grumpkin_point::GrumpkinPoint, constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL @@ -153,7 +154,7 @@ impl Deck<&mut PrivateContext> { } } -impl Deck<()> { +impl Deck { unconstrained pub fn view_cards(self, offset: u32) -> [Option; MAX_NOTES_PER_PAGE] { let mut options = NoteViewerOptions::new(); let opt_notes = self.set.view_notes(options.set_offset(offset)); diff --git a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr index bee8167a299a..0e3084fd7c3c 100644 --- a/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr +++ b/noir-projects/noir-contracts/contracts/token_blacklist_contract/src/types/balances_map.nr @@ -1,9 +1,7 @@ -use dep::aztec::prelude::{ - AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateContext, - PrivateSet, Map -}; +use dep::aztec::prelude::{AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateSet, Map}; use dep::aztec::{ - hash::pedersen_hash, protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, + context::{PrivateContext, UnconstrainedContext}, hash::pedersen_hash, + protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, note::{note_getter::view_notes, note_getter_options::SortOrder} }; use crate::types::token_note::{TokenNote, OwnedNote}; @@ -25,7 +23,7 @@ impl BalancesMap { } } -impl BalancesMap { +impl BalancesMap { unconstrained pub fn balance_of( self: Self, owner: AztecAddress diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr b/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr index bee8167a299a..0e3084fd7c3c 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/types/balances_map.nr @@ -1,9 +1,7 @@ -use dep::aztec::prelude::{ - AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateContext, - PrivateSet, Map -}; +use dep::aztec::prelude::{AztecAddress, NoteGetterOptions, NoteViewerOptions, NoteHeader, NoteInterface, PrivateSet, Map}; use dep::aztec::{ - hash::pedersen_hash, protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, + context::{PrivateContext, UnconstrainedContext}, hash::pedersen_hash, + protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, note::{note_getter::view_notes, note_getter_options::SortOrder} }; use crate::types::token_note::{TokenNote, OwnedNote}; @@ -25,7 +23,7 @@ impl BalancesMap { } } -impl BalancesMap { +impl BalancesMap { unconstrained pub fn balance_of( self: Self, owner: AztecAddress diff --git a/noir/noir-repo/aztec_macros/src/transforms/functions.rs b/noir/noir-repo/aztec_macros/src/transforms/functions.rs index 0fb13975fec0..4d8b6ef7cdfc 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/functions.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/functions.rs @@ -216,12 +216,30 @@ pub fn export_fn_abi( /// /// Inserts the following code at the beginning of an unconstrained function /// ```noir -/// let storage = Storage::init(Context::none()); +/// let context = UnconstrainedContext::new(); +/// let storage = Storage::init(context); /// ``` /// /// This will allow developers to access their contract' storage struct in unconstrained functions pub fn transform_unconstrained(func: &mut NoirFunction, storage_struct_name: String) { + // let context = UnconstrainedContext::new(); + let let_context = assignment( + "context", // Assigned to + call( + variable_path(chained_dep!( + "aztec", + "context", + "unconstrained_context", + "UnconstrainedContext", + "new" + )), + vec![], + ), + ); + + // We inject the statements at the beginning, in reverse order. func.def.body.statements.insert(0, abstract_storage(storage_struct_name, true)); + func.def.body.statements.insert(0, let_context); } /// Helper function that returns what the private context would look like in the ast @@ -597,7 +615,7 @@ fn abstract_return_values(func: &NoirFunction) -> Result>, /// ```noir /// #[aztec(private)] /// fn lol() { -/// let storage = Storage::init(context); +/// let storage = Storage::init(&mut context); /// } /// ``` /// @@ -605,22 +623,18 @@ fn abstract_return_values(func: &NoirFunction) -> Result>, /// ```noir /// #[aztec(public)] /// fn lol() { -/// let storage = Storage::init(context); +/// let storage = Storage::init(&mut context); /// } /// ``` /// /// For unconstrained functions: /// ```noir /// unconstrained fn lol() { -/// let storage = Storage::init(()); +/// let storage = Storage::init(context); /// } fn abstract_storage(storage_struct_name: String, unconstrained: bool) -> Statement { - let context_expr = if unconstrained { - // Note that the literal unit type (i.e. '()') is not the same as a tuple with zero elements - expression(ExpressionKind::Literal(Literal::Unit)) - } else { - mutable_reference("context") - }; + let context_expr = + if unconstrained { variable("context") } else { mutable_reference("context") }; assignment( "storage", // Assigned to diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 0a76bb2e77fb..1bd7a445ac65 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -41,6 +41,14 @@ export class Oracle { return unpacked.map(toACVMField); } + async getBlockNumber(): Promise { + return toACVMField(await this.typedOracle.getBlockNumber()); + } + + async getContractAddress(): Promise { + return toACVMField(await this.typedOracle.getContractAddress()); + } + async getKeyValidationRequest([pkMHash]: ACVMField[]): Promise { const { pkM, skApp } = await this.typedOracle.getKeyValidationRequest(fromACVMField(pkMHash)); diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 90e923475e56..230b82b180a1 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -82,6 +82,14 @@ export abstract class TypedOracle { throw new OracleMethodNotAvailableError('unpackReturns'); } + getBlockNumber(): Promise { + throw new OracleMethodNotAvailableError('getBlockNumber'); + } + + getContractAddress(): Promise { + throw new OracleMethodNotAvailableError('getContractAddress'); + } + getKeyValidationRequest(_pkMHash: Fr): Promise { throw new OracleMethodNotAvailableError('getKeyValidationRequest'); } diff --git a/yarn-project/simulator/src/client/unconstrained_execution.test.ts b/yarn-project/simulator/src/client/unconstrained_execution.test.ts index 788da2d37bcb..0f97b50ec429 100644 --- a/yarn-project/simulator/src/client/unconstrained_execution.test.ts +++ b/yarn-project/simulator/src/client/unconstrained_execution.test.ts @@ -17,6 +17,10 @@ describe('Unconstrained Execution test suite', () => { beforeEach(() => { oracle = mock(); + + node = mock(); + node.getBlockNumber.mockResolvedValue(42); + acirSimulator = new AcirSimulator(oracle, node); }); diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index 47a75f4ad2c6..f70e8db09b80 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -34,6 +34,14 @@ export class ViewDataOracle extends TypedOracle { super(); } + public override getBlockNumber(): Promise { + return this.aztecNode.getBlockNumber(); + } + + public override getContractAddress(): Promise { + return Promise.resolve(this.contractAddress); + } + /** * Retrieve keys associated with a specific master public key and app address. * @param pkMHash - The master public key hash.