Skip to content

Commit

Permalink
feat!: TXE single execution env (#9183)
Browse files Browse the repository at this point in the history
## TXE single execution env

Previously, TXE had a weird dual model in which direct calls from test
code would "inline" contract code and call it directly. This was very
fast, but unfortunately wasn't possible when doing an external call.

`test -> env.call(ContractUnderTest.fn) (inlined call) ->
ExternalContract::at(addr).fn (nested call inside ACVM or AVM
simulator)`

This forced us to reproduce the simulator behavior both in Noir and in
TXE, leading to bugs and generally terrible UX. The upside is that
besides being faster, it also allowed the developer to *NOT* recompile
the contract code on every change, only when modifying external
contracts that were called from the one under test.

Fortunately, recent Brillig improvements allow us to do every single
call (including inside tests) as a nested call, which leads to a lot of
deduplication and a more consistent UX. Contracts under test now have to
*always* be recompiled and deployed, but it is a small price to pay for
consistency's sake.

## Public rethrows and proper support for
`#[test(should_fail_with="message")]`

Previously, the very weird dual model introduced problems when trying to
parse errors thrown by public functions, specially when they were nested
calls. This is *NO MORE* and we can always use Noir's native system to
parse error messages and ensure negative tests fail *properly*

## Call interface rework

Since TXE has been simplified, call interfaces don't need the weird
`original: fn` field that previously allowed use to call the "macrofied"
version of the contract function inline with the test. However, this
rework led to the discovery of an issue: return types from call
interfaces were incorrectly typed, and were only bound by the compiler
when used after the call. This has been fixed by adding a `zeroed`
return_value field to the call interfaces (essentially rust's
`PhantomData`) so we are able to properly type them and leverage the
type system to ensure the values are not only what we expect, but of the
type we expect.
  • Loading branch information
Thunkar authored Oct 23, 2024
1 parent 1ce0fa5 commit 1d1d76d
Show file tree
Hide file tree
Showing 40 changed files with 714 additions and 929 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ let my_contract_instance = deployer.without_initializer();
```

:::warning
It is not always necessary to deploy a contract in order to test it, but sometimes it's inevitable (when testing functions that depend on the contract being initialized, or contracts that call others for example) **It is important to keep them up to date**, as TXE cannot recompile them on changes. Think of it as regenerating the bytecode and ABI so it becomes accessible externally.
It is always necessary to deploy a contract in order to test it. **It is important to keep them up to date**, as TXE cannot recompile them on changes. Think of it as regenerating the bytecode and ABI so it becomes accessible externally.
:::

### Calling functions
Expand Down Expand Up @@ -210,7 +210,7 @@ For example:

You can also use the `assert_public_call_fails` or `assert_private_call_fails` methods on the `TestEnvironment` to check that a call fails.

#include_code assert_public_fail /noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_public.nr rust
#include_code assert_public_fail /noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr rust

### Logging

Expand Down
12 changes: 12 additions & 0 deletions docs/docs/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ keywords: [sandbox, aztec, notes, migration, updating, upgrading]

Aztec is in full-speed development. Literally every version breaks compatibility with the previous ones. This page attempts to target errors and difficulties you might encounter when upgrading, and how to resolve them.

## 0.X.X
### [TXE] Single execution environment
Thanks to recent advancements in Brillig TXE performs every single call as if it was a nested call, spawning a new ACVM or AVM simulator without performance loss.
This ensures every single test runs in a consistent environment and allows for clearer test syntax:

```diff
-let my_call_interface = MyContract::at(address).my_function(args);
-env.call_private(my_contract_interface)
+MyContract::at(address).my_function(args).call(&mut env.private());
```
This implies every contract has to be deployed before it can be tested (via `env.deploy` or `env.deploy_self`) and of course it has to be recompiled if its code was changed before TXE can use the modified bytecode.

## 0.58.0
### [l1-contracts] Inbox's MessageSent event emits global tree index
Earlier `MessageSent` event in Inbox emitted a subtree index (index of the message in the subtree of the l2Block). But the nodes and Aztec.nr expects the index in the global L1_TO_L2_MESSAGES_TREE. So to make it easier to parse this, Inbox now emits this global index.
Expand Down
10 changes: 3 additions & 7 deletions noir-projects/aztec-nr/authwit/src/cheatcodes.nr
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ use dep::aztec::{

use crate::auth::{compute_inner_authwit_hash, compute_authwit_message_hash, set_authorized};

pub fn add_private_authwit_from_call_interface<C, let M: u32, T, P, Env>(
on_behalf_of: AztecAddress,
caller: AztecAddress,
call_interface: C
) where C: CallInterface<M, T, P, Env> {
pub fn add_private_authwit_from_call_interface<C, let M: u32>(on_behalf_of: AztecAddress, caller: AztecAddress, call_interface: C) where C: CallInterface<M> {
let target = call_interface.get_contract_address();
let inputs = cheatcodes::get_private_context_inputs(get_block_number());
let chain_id = inputs.tx_context.chain_id;
Expand All @@ -22,11 +18,11 @@ pub fn add_private_authwit_from_call_interface<C, let M: u32, T, P, Env>(
cheatcodes::add_authwit(on_behalf_of, message_hash);
}

pub fn add_public_authwit_from_call_interface<C, let M: u32, T, P, Env>(
pub fn add_public_authwit_from_call_interface<C, let M: u32>(
on_behalf_of: AztecAddress,
caller: AztecAddress,
call_interface: C
) where C: CallInterface<M, T, P, Env> {
) where C: CallInterface<M> {
let current_contract = get_contract_address();
cheatcodes::set_contract_address(on_behalf_of);
let target = call_interface.get_contract_address();
Expand Down
105 changes: 33 additions & 72 deletions noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use dep::protocol_types::{
abis::{function_selector::FunctionSelector, private_circuit_public_inputs::PrivateCircuitPublicInputs},
address::AztecAddress, traits::Deserialize
};
use dep::protocol_types::{abis::{function_selector::FunctionSelector}, address::AztecAddress, traits::Deserialize};

use crate::context::{
private_context::PrivateContext, public_context::PublicContext, gas::GasOpts,
Expand All @@ -11,9 +8,7 @@ use crate::context::{
use crate::oracle::arguments::pack_arguments;
use crate::hash::hash_args;

pub trait CallInterface<let N: u32, T, P, Env> {
fn get_original(self) -> fn[Env](T) -> P;

pub trait CallInterface<let N: u32> {
fn get_args(self) -> [Field] {
self.args
}
Expand All @@ -35,23 +30,17 @@ pub trait CallInterface<let N: u32, T, P, Env> {
}
}

impl<let N: u32, T, P, Env> CallInterface<N, PrivateContextInputs, PrivateCircuitPublicInputs, Env> for PrivateCallInterface<N, T, Env> {
fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs {
self.original
}
}

pub struct PrivateCallInterface<let N: u32, T, Env> {
pub struct PrivateCallInterface<let N: u32, T> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args_hash: Field,
args: [Field],
original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs,
return_type: T,
is_static: bool
}

impl<let N: u32, T, Env> PrivateCallInterface<N, T, Env> {
impl<let N: u32, T> PrivateCallInterface<N, T> {
pub fn call<let M: u32>(self, context: &mut PrivateContext) -> T where T: Deserialize<M> {
pack_arguments(self.args);
let returns = context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, false);
Expand All @@ -66,23 +55,19 @@ impl<let N: u32, T, Env> PrivateCallInterface<N, T, Env> {
}
}

impl<let N: u32, T, P, Env> CallInterface<N, PrivateContextInputs, PrivateCircuitPublicInputs, Env> for PrivateVoidCallInterface<N, Env> {
fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs {
self.original
}
}
impl<let N: u32> CallInterface<N> for PrivateVoidCallInterface<N> {}

pub struct PrivateVoidCallInterface<let N: u32, Env> {
pub struct PrivateVoidCallInterface<let N: u32> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args_hash: Field,
args: [Field],
original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs,
return_type: (),
is_static: bool
}

impl<let N: u32, Env> PrivateVoidCallInterface<N, Env> {
impl<let N: u32> PrivateVoidCallInterface<N> {
pub fn call(self, context: &mut PrivateContext) {
pack_arguments(self.args);
context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, false).assert_empty();
Expand All @@ -94,70 +79,58 @@ impl<let N: u32, Env> PrivateVoidCallInterface<N, Env> {
}
}

impl<let N: u32, T, P, Env> CallInterface<N, PrivateContextInputs, PrivateCircuitPublicInputs, Env> for PrivateStaticCallInterface<N, T, Env> {
fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs {
self.original
}
}
impl<let N: u32, T> CallInterface<N> for PrivateStaticCallInterface<N, T> {}

pub struct PrivateStaticCallInterface<let N: u32, T, Env> {
pub struct PrivateStaticCallInterface<let N: u32, T> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args_hash: Field,
args: [Field],
original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs,
return_type: T,
is_static: bool
}

impl<let N: u32, T, Env> PrivateStaticCallInterface<N, T, Env> {
impl<let N: u32, T> PrivateStaticCallInterface<N, T> {
pub fn view<let M: u32>(self, context: &mut PrivateContext) -> T where T: Deserialize<M> {
pack_arguments(self.args);
let returns = context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, true);
returns.unpack_into()
}
}

impl<let N: u32, T, P, Env> CallInterface<N, PrivateContextInputs, PrivateCircuitPublicInputs, Env> for PrivateStaticVoidCallInterface<N, Env> {
fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs {
self.original
}
}
impl<let N: u32> CallInterface<N> for PrivateStaticVoidCallInterface<N> {}

pub struct PrivateStaticVoidCallInterface<let N: u32, Env> {
pub struct PrivateStaticVoidCallInterface<let N: u32> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args_hash: Field,
args: [Field],
original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs,
return_type: (),
is_static: bool
}

impl<let N: u32, Env> PrivateStaticVoidCallInterface<N, Env> {
impl<let N: u32> PrivateStaticVoidCallInterface<N> {
pub fn view(self, context: &mut PrivateContext) {
pack_arguments(self.args);
context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, true).assert_empty();
}
}

impl<let N: u32, T, P, Env> CallInterface<N, (), T, Env> for PublicCallInterface<N, T, Env> {
fn get_original(self) -> fn[Env](()) -> T {
self.original
}
}
impl<let N: u32, T> CallInterface<N> for PublicCallInterface<N, T> {}

pub struct PublicCallInterface<let N: u32, T, Env> {
pub struct PublicCallInterface<let N: u32, T> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args: [Field],
gas_opts: GasOpts,
original: fn[Env](()) -> T,
return_type: T,
is_static: bool
}

impl<let N: u32, T, Env> PublicCallInterface<N, T, Env> {
impl<let N: u32, T> PublicCallInterface<N, T> {
pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self {
self.gas_opts = gas_opts;
self
Expand Down Expand Up @@ -196,23 +169,19 @@ impl<let N: u32, T, Env> PublicCallInterface<N, T, Env> {
}
}

impl<let N: u32, T, P, Env> CallInterface<N, (), (), Env> for PublicVoidCallInterface<N, Env> {
fn get_original(self) -> fn[Env](()) -> () {
self.original
}
}
impl<let N: u32> CallInterface<N> for PublicVoidCallInterface<N> {}

pub struct PublicVoidCallInterface<let N: u32, Env> {
pub struct PublicVoidCallInterface<let N: u32> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args: [Field],
original: fn[Env](()) -> (),
return_type: (),
is_static: bool,
gas_opts: GasOpts
}

impl<let N: u32, Env> PublicVoidCallInterface<N, Env> {
impl<let N: u32> PublicVoidCallInterface<N> {
pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self {
self.gas_opts = gas_opts;
self
Expand Down Expand Up @@ -251,23 +220,19 @@ impl<let N: u32, Env> PublicVoidCallInterface<N, Env> {
}
}

impl<let N: u32, T, P, Env> CallInterface<N, (), T, Env> for PublicStaticCallInterface<N, T, Env> {
fn get_original(self) -> fn[Env](()) -> T {
self.original
}
}
impl<let N: u32, T> CallInterface<N> for PublicStaticCallInterface<N, T> {}

pub struct PublicStaticCallInterface<let N: u32, T, Env> {
pub struct PublicStaticCallInterface<let N: u32, T> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args: [Field],
original: fn[Env](()) -> T,
return_type: T,
is_static: bool,
gas_opts: GasOpts
}

impl<let N: u32, T, Env> PublicStaticCallInterface<N, T, Env> {
impl<let N: u32, T> PublicStaticCallInterface<N, T> {
pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self {
self.gas_opts = gas_opts;
self
Expand All @@ -291,23 +256,19 @@ impl<let N: u32, T, Env> PublicStaticCallInterface<N, T, Env> {
}
}

impl<let N: u32, T, P, Env> CallInterface<N, (), (), Env> for PublicStaticVoidCallInterface<N, Env> {
fn get_original(self) -> fn[Env](()) -> () {
self.original
}
}
impl<let N: u32> CallInterface<N> for PublicStaticVoidCallInterface<N> {}

pub struct PublicStaticVoidCallInterface<let N: u32, Env> {
pub struct PublicStaticVoidCallInterface<let N: u32> {
target_contract: AztecAddress,
selector: FunctionSelector,
name: str<N>,
args: [Field],
original: fn[Env](()) -> (),
return_type: (),
is_static: bool,
gas_opts: GasOpts
}

impl<let N: u32, Env> PublicStaticVoidCallInterface<N, Env> {
impl<let N: u32> PublicStaticVoidCallInterface<N> {
pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self {
self.gas_opts = gas_opts;
self
Expand Down
46 changes: 3 additions & 43 deletions noir-projects/aztec-nr/aztec/src/macros/functions/interfaces.nr
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,10 @@ pub comptime fn stub_fn(f: FunctionDefinition) -> Quoted {

let fn_name_len: u32 = unquote!(quote { $fn_name_str.as_bytes().len()});

let arg_types_list: Quoted = fn_parameters.map(|(_, typ): (_, Type)| quote { $typ }).join(quote {,});
let arg_types = if fn_parameters.len() == 1 {
// Extra colon to avoid it being interpreted as a parenthesized expression instead of a tuple
quote { ($arg_types_list,) }
} else {
quote { ($arg_types_list) }
};

let call_interface_generics = if is_void {
quote { $fn_name_len, $arg_types }
quote { $fn_name_len }
} else {
quote { $fn_name_len, $fn_return_type, $arg_types }
quote { $fn_name_len, $fn_return_type }
};

let call_interface_name = f"dep::aztec::context::call_interfaces::{fn_visibility_capitalized}{is_static_call_capitalized}{is_void_capitalized}CallInterface".quoted_contents();
Expand All @@ -128,38 +120,6 @@ pub comptime fn stub_fn(f: FunctionDefinition) -> Quoted {
quote {}
};

let input_type = if is_fn_private(f) {
quote { crate::context::inputs::PrivateContextInputs }.as_type()
} else {
quote { () }.as_type()
};

let return_type_hint = if is_fn_private(f) {
quote { protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs }.as_type()
} else {
fn_return_type
};

let mut parameter_names_list = fn_parameters.map(|(name, _): (Quoted, _)| name);
let parameter_names = if is_fn_private(f) {
&[quote {inputs}].append(parameter_names_list).join(quote{,})
} else {
parameter_names_list.join(quote {,})
};
let original = if is_fn_private(f) {
quote {
| inputs: $input_type | -> $return_type_hint {
$fn_name($parameter_names)
}
}
} else {
quote {
| _: $input_type | -> $return_type_hint {
unsafe { $fn_name($parameter_names) }
}
}
};

let args_hash = if fn_visibility == quote { private } {
quote { $args_hash_name, }
} else {
Expand All @@ -176,7 +136,7 @@ pub comptime fn stub_fn(f: FunctionDefinition) -> Quoted {
name: $fn_name_str,
$args_hash
args: $args_acc_name,
original: $original,
return_type: std::mem::zeroed(),
is_static: $is_static_call,
$gas_opts
}
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/macros/functions/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ pub comptime fn private(f: FunctionDefinition) -> Quoted {

let mut body = f.body().as_block().unwrap();

// The original params are hashed and passed to the `context` object, so that the kernel can verify we're received
// The original params are hashed and passed to the `context` object, so that the kernel can verify we've received
// the correct values.

// TODO: Optimize args_hasher for small number of arguments
Expand Down
5 changes: 4 additions & 1 deletion noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use crate::utils::comparison::Comparator;
global storage_slot: Field = 42;

unconstrained fn setup() -> TestEnvironment {
TestEnvironment::new()
let mut env = TestEnvironment::new();
// Advance 1 block so we can read historic state from private
env.advance_block_by(1);
env
}

unconstrained fn build_valid_note(value: Field) -> MockNote {
Expand Down
Loading

0 comments on commit 1d1d76d

Please sign in to comment.