Skip to content

Commit

Permalink
feat!: introduce UnconstrainedContext (#6752)
Browse files Browse the repository at this point in the history
Continuing the work from
#6442, this PR
further formalizes the notion of a top-level unconstrained execution
context by introducing a struct that represents it (instead of relying
on the unit type). Not only is this less cryptic, it also provides
access to data previously unavailable such as the current block number
and contract address, which we'll need for some unconstrained getters
like `SharedMutable`'s.

The macro functions could potentially be refactored somewhat now that
private, public and unconstrained are more similar, but I'm not sure we
want to invest much effort there so I made the change as small as
possible.
  • Loading branch information
nventuro authored Jun 3, 2024
1 parent 1324d58 commit e00b251
Show file tree
Hide file tree
Showing 19 changed files with 140 additions and 49 deletions.
11 changes: 11 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<UnconstrainedContext> {
```

### [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.
Expand Down
7 changes: 4 additions & 3 deletions docs/docs/reference/smart_contract_reference/storage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod inputs;
mod packed_returns;
mod private_context;
mod public_context;
mod unconstrained_context;

mod call_interfaces;
mod gas;

Expand All @@ -16,3 +18,4 @@ use private_context::PrivateContext;
use packed_returns::PackedReturns;
use public_context::PublicContext;
use public_context::FunctionReturns;
use unconstrained_context::UnconstrainedContext;
32 changes: 32 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context/unconstrained_context.nr
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,7 +66,7 @@ impl<Note> PrivateImmutable<Note, &mut PrivateContext> {
// docs:end:get_note
}

impl<Note> PrivateImmutable<Note, ()> {
impl<Note> PrivateImmutable<Note, UnconstrainedContext> {
// docs:start:is_initialized
unconstrained pub fn is_initialized(self) -> bool {
let nullifier = self.compute_initialization_nullifier();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -124,7 +124,7 @@ impl<Note> PrivateMutable<Note, &mut PrivateContext> {
// docs:end:get_note
}

impl<Note> PrivateMutable<Note, ()> {
impl<Note> PrivateMutable<Note, UnconstrainedContext> {
unconstrained pub fn is_initialized(self) -> bool {
let nullifier = self.compute_initialization_nullifier();
check_nullifier_exists(nullifier)
Expand Down
4 changes: 2 additions & 2 deletions noir-projects/aztec-nr/aztec/src/state_vars/private_set.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -85,7 +85,7 @@ impl<Note> PrivateSet<Note, &mut PrivateContext> {
// docs:end:get_notes
}

impl<Note> PrivateSet<Note, ()> {
impl<Note> PrivateSet<Note, UnconstrainedContext> {
// docs:start:view_notes
unconstrained pub fn view_notes<N, M>(
self,
Expand Down
13 changes: 8 additions & 5 deletions noir-projects/aztec-nr/aztec/src/state_vars/public_immutable.nr
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -54,11 +57,11 @@ impl <T> PublicImmutable<T, &mut PublicContext> {
// docs:end:public_immutable_struct_read
}

impl<T> PublicImmutable<T, ()> {
impl<T> PublicImmutable<T, UnconstrainedContext> {
pub fn read<T_SERIALIZED_LEN>(self) -> T where T: Deserialize<T_SERIALIZED_LEN> {
// 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)
}
Expand Down
10 changes: 5 additions & 5 deletions noir-projects/aztec-nr/aztec/src/state_vars/public_mutable.nr
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -42,11 +42,11 @@ impl<T> PublicMutable<T, &mut PublicContext> {
// docs:end:public_mutable_struct_write
}

impl<T> PublicMutable<T, ()> {
impl<T> PublicMutable<T, UnconstrainedContext> {
pub fn read<T_SERIALIZED_LEN>(self) -> T where T: Deserialize<T_SERIALIZED_LEN> {
// 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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}};

Expand Down Expand Up @@ -49,7 +49,7 @@ impl<T> SharedImmutable<T, &mut PublicContext> {
}
}

impl<T> SharedImmutable<T, ()> {
impl<T> SharedImmutable<T, UnconstrainedContext> {
pub fn read_public<T_SERIALIZED_LEN>(self) -> T where T: Deserialize<T_SERIALIZED_LEN> {
let fields = storage_read(self.storage_slot);
T::deserialize(fields)
Expand Down
10 changes: 6 additions & 4 deletions noir-projects/aztec-nr/value-note/src/balance_utils.nr
Original file line number Diff line number Diff line change
@@ -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<ValueNote, ()>) -> Field {
unconstrained pub fn get_balance(set: PrivateSet<ValueNote, UnconstrainedContext>) -> Field {
get_balance_with_offset(set, 0)
}

unconstrained pub fn get_balance_with_offset(set: PrivateSet<ValueNote, ()>, offset: u32) -> Field {
unconstrained pub fn get_balance_with_offset(set: PrivateSet<ValueNote, UnconstrainedContext>, offset: u32) -> Field {
let mut balance = 0;
// docs:start:view_notes
let mut options = NoteViewerOptions::new();
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -153,7 +154,7 @@ impl Deck<&mut PrivateContext> {
}
}

impl Deck<()> {
impl Deck<UnconstrainedContext> {
unconstrained pub fn view_cards(self, offset: u32) -> [Option<Card>; MAX_NOTES_PER_PAGE] {
let mut options = NoteViewerOptions::new();
let opt_notes = self.set.view_notes(options.set_offset(offset));
Expand Down
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -25,7 +23,7 @@ impl<T, Context> BalancesMap<T, Context> {
}
}

impl<T> BalancesMap<T, ()> {
impl<T> BalancesMap<T, UnconstrainedContext> {
unconstrained pub fn balance_of<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
owner: AztecAddress
Expand Down
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -25,7 +23,7 @@ impl<T, Context> BalancesMap<T, Context> {
}
}

impl<T> BalancesMap<T, ()> {
impl<T> BalancesMap<T, UnconstrainedContext> {
unconstrained pub fn balance_of<T_SERIALIZED_LEN, T_SERIALIZED_BYTES_LEN>(
self: Self,
owner: AztecAddress
Expand Down
34 changes: 24 additions & 10 deletions noir/noir-repo/aztec_macros/src/transforms/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -597,30 +615,26 @@ fn abstract_return_values(func: &NoirFunction) -> Result<Option<Vec<Statement>>,
/// ```noir
/// #[aztec(private)]
/// fn lol() {
/// let storage = Storage::init(context);
/// let storage = Storage::init(&mut context);
/// }
/// ```
///
/// For public functions:
/// ```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
Expand Down
8 changes: 8 additions & 0 deletions yarn-project/simulator/src/acvm/oracle/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export class Oracle {
return unpacked.map(toACVMField);
}

async getBlockNumber(): Promise<ACVMField> {
return toACVMField(await this.typedOracle.getBlockNumber());
}

async getContractAddress(): Promise<ACVMField> {
return toACVMField(await this.typedOracle.getContractAddress());
}

async getKeyValidationRequest([pkMHash]: ACVMField[]): Promise<ACVMField[]> {
const { pkM, skApp } = await this.typedOracle.getKeyValidationRequest(fromACVMField(pkMHash));

Expand Down
8 changes: 8 additions & 0 deletions yarn-project/simulator/src/acvm/oracle/typed_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ export abstract class TypedOracle {
throw new OracleMethodNotAvailableError('unpackReturns');
}

getBlockNumber(): Promise<number> {
throw new OracleMethodNotAvailableError('getBlockNumber');
}

getContractAddress(): Promise<AztecAddress> {
throw new OracleMethodNotAvailableError('getContractAddress');
}

getKeyValidationRequest(_pkMHash: Fr): Promise<KeyValidationRequest> {
throw new OracleMethodNotAvailableError('getKeyValidationRequest');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ describe('Unconstrained Execution test suite', () => {

beforeEach(() => {
oracle = mock<DBOracle>();

node = mock<AztecNode>();
node.getBlockNumber.mockResolvedValue(42);

acirSimulator = new AcirSimulator(oracle, node);
});

Expand Down
Loading

0 comments on commit e00b251

Please sign in to comment.