Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: introduce UnconstrainedContext #6752

Merged
merged 6 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading