Skip to content

Commit

Permalink
feat: router contract (#8352)
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan authored Sep 4, 2024
1 parent e1dc987 commit 138dc52
Show file tree
Hide file tree
Showing 36 changed files with 510 additions and 229 deletions.
22 changes: 16 additions & 6 deletions docs/docs/aztec/glossary/call_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,31 @@ Unlike the EVM however, private execution doesn't revert in the traditional way:

Since public execution can only be performed by the sequencer, public functions cannot be executed in a private context. It is possible however to _enqueue_ a public function call during private execution, requesting the sequencer to run it during inclusion of the transaction. It will be [executed in public](#public-execution) normally, including the possibility to enqueue static public calls.

Since the public call is made asynchronously, any return values or side effects are not available during private execution. If the public function fails once executed, the entire transaction is reverted inncluding state changes caused by the private part, such as new notes or nullifiers. Note that this does result in gas being spent, like in the case of the EVM.
Since the public call is made asynchronously, any return values or side effects are not available during private execution. If the public function fails once executed, the entire transaction is reverted including state changes caused by the private part, such as new notes or nullifiers. Note that this does result in gas being spent, like in the case of the EVM.

#include_code enqueue_public /noir-projects/noir-contracts/contracts/lending_contract/src/main.nr rust

It is also possible to create public functions that can _only_ be invoked by privately enqueing a call from the same contract, which can very useful to update public state after private exection (e.g. update a token's supply after privately minting). This is achieved by annotating functions with `#[aztec(internal)]`.
It is also possible to create public functions that can _only_ be invoked by privately enqueueing a call from the same contract, which can very useful to update public state after private execution (e.g. update a token's supply after privately minting). This is achieved by annotating functions with `#[aztec(internal)]`.

A common pattern is to enqueue public calls to check some validity condition on public state, e.g. that a deadline has not expired or that some public value is set.

#include_code enqueueing /noir-projects/noir-contracts/contracts/router_contract/src/main.nr rust

Note that this reveals what public function is being called on what contract.
For this reason we've created a canonical router contract which implements some of the checks commonly performed.
This conceals what contract performed the public call as the `context.msg_sender()` in the public function is the router itself (since the router's private function enqueued the public call).

An example of how a deadline can be checked using the router contract follows:

#include_code call-check-deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

#include_code deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust
This is what the implementation of the check timestamp functionality looks like:

:::warning
Calling public functions privately leaks some privacy! The caller of the function and all arguments will be revelead, so exercise care when mixing the private and public domains. To learn about alternative ways to access public state privately, look into [Shared State](../../reference/developer_references/smart_contract_reference/storage/shared_state.md).
:::
#include_code check_timestamp /noir-projects/noir-contracts/contracts/router_contract/src/main.nr rust

Even with the router contract achieving good privacy is hard.
This is especially the case when the value being checked is unique and stored in the contract's public storage.
For this reason it is encouraged to try to avoid public function calls and instead privately read [Shared State](../../reference/developer_references/smart_contract_reference/storage/shared_state.md) when possible.

### Public Execution

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,29 +131,13 @@ You can compile the code so far with `aztec-nargo compile`.

To check that the donation occurs before the campaign deadline, we must access the public `timestamp`. It is one of several public global variables.

Declare an Aztec function that is public and internal
We read the deadline from public storage in private and use the router contract to assert that the current `timestamp` is before the deadline.

```rust
#include_code deadline-header /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr raw
//...
}
```

Read the deadline from storage and assert that the `timestamp` from this context is before the deadline

#include_code deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

---

Since donations are to be private, the donate function will have the user's private context which has these private global variables. So from the private context there is a little extra to call the (public internal) `_check_deadline` function.

```rust
#include_code call-check-deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr raw
//...
}
```
#include_code call-check-deadline /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

Namely calling `enqueue` and passing the (mutable) context.
We do the check via the router contract to conceal which contract is performing the check (This is achieved by calling a private function on the router contract which then enqueues a call to a public function on the router contract. This then results in the msg_sender in the public call being the router contract.)
Note that the privacy here is dependent upon what deadline value is chosen by the Crowdfunding contract deployer.
If it's unique to this contract, then we are leaking a privacy.

Now conclude adding all dependencies to the `Crowdfunding` contract:

Expand Down Expand Up @@ -193,7 +177,7 @@ Copy the last function into your Crowdfunding contract:

#include_code operator-withdrawals /noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr rust

You should be able to compile successfully with `aztec-nargo compile`.
You should be able to compile successfully with `aztec-nargo compile`.

**Congratulations,** you have just built a multi-contract project on Aztec!

Expand All @@ -206,7 +190,7 @@ See [claim_contract (GitHub link)](https://github.com/AztecProtocol/aztec-packag

## Next steps

### Build an accounts contract
### Build an accounts contract

Follow the account contract tutorial on the [next page](./write_accounts_contract.md) and learn more about account abstraction.

Expand Down
2 changes: 2 additions & 0 deletions l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ library Constants {
2631409926445785927331173506476539962589925110142857699603561302478860342858;
uint256 internal constant FEE_JUICE_ADDRESS =
10248142274714515101077825679585135641434041564851038865006795089686437446849;
uint256 internal constant ROUTER_ADDRESS =
8135649085127523915405560812661632604783066942985338123941332115593181690668;
uint256 internal constant AZTEC_ADDRESS_LENGTH = 1;
uint256 internal constant GAS_FEES_LENGTH = 2;
uint256 internal constant GAS_LENGTH = 2;
Expand Down
28 changes: 9 additions & 19 deletions noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use dep::protocol_types::{constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, GET_
use crate::context::PrivateContext;
use crate::note::{
constants::{GET_NOTE_ORACLE_RETURN_LENGTH, MAX_NOTES_PER_PAGE, VIEW_NOTE_ORACLE_RETURN_LENGTH},
note_getter_options::{NoteGetterOptions, Select, Sort, SortOrder, Comparator, NoteStatus, PropertySelector},
note_getter_options::{NoteGetterOptions, Select, Sort, SortOrder, NoteStatus, PropertySelector},
note_interface::NoteInterface, note_viewer_options::NoteViewerOptions,
utils::compute_note_hash_for_read_request
};
use crate::oracle;
use crate::utils::comparison::assert_comparison;

mod test;

Expand Down Expand Up @@ -50,23 +51,12 @@ fn check_note_fields<let N: u32>(
let select = selects.get_unchecked(i).unwrap_unchecked();
let value_field = extract_property_value_from_selector(serialized_note, select.property_selector);

// Values are computed ahead of time because circuits evaluate all branches
let is_equal = value_field == select.value.to_field();
let is_lt = value_field.lt(select.value.to_field());

if (select.comparator == Comparator.EQ) {
assert(is_equal, "Mismatch return note field.");
} else if (select.comparator == Comparator.NEQ) {
assert(!is_equal, "Mismatch return note field.");
} else if (select.comparator == Comparator.LT) {
assert(is_lt, "Mismatch return note field.");
} else if (select.comparator == Comparator.LTE) {
assert(is_lt | is_equal, "Mismatch return note field.");
} else if (select.comparator == Comparator.GT) {
assert(!is_lt & !is_equal, "Mismatch return note field.");
} else if (select.comparator == Comparator.GTE) {
assert(!is_lt, "Mismatch return note field.");
}
assert_comparison(
value_field,
select.comparator,
select.value.to_field(),
"Mismatch return note field."
);
}
}

Expand Down Expand Up @@ -135,7 +125,7 @@ fn constrain_get_notes_internal<Note, let N: u32, let M: u32, PREPROCESSOR_ARGS,
let filter_args = options.filter_args;
let filtered_notes = filter_fn(opt_notes, filter_args);

let notes = crate::utils::collapse(filtered_notes);
let notes = crate::utils::collapse::collapse_array(filtered_notes);
let mut note_hashes: BoundedVec<Field, MAX_NOTE_HASH_READ_REQUESTS_PER_CALL> = BoundedVec::new();

// We have now collapsed the sparse array of Options into a BoundedVec. This is a more ergonomic type and also
Expand Down
6 changes: 2 additions & 4 deletions noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
use dep::protocol_types::constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL;
use crate::{
context::PrivateContext,
note::{
note_header::NoteHeader,
note_getter_options::{NoteGetterOptions, Sort, SortOrder, Comparator, PropertySelector},
note_getter_options::{NoteGetterOptions, SortOrder, PropertySelector},
note_getter::constrain_get_notes_internal
},
oracle::execution::get_contract_address
};
use dep::protocol_types::address::AztecAddress;

use crate::test::{helpers::test_environment::TestEnvironment, mocks::mock_note::MockNote};
use crate::utils::comparison::Comparator;

global storage_slot: Field = 42;

Expand Down
19 changes: 1 addition & 18 deletions noir-projects/aztec-nr/aztec/src/note/note_getter_options.nr
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
use std::option::Option;
use dep::protocol_types::{constants::MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, traits::ToField};
use crate::note::note_interface::NoteInterface;
use crate::utils::comparison::Comparator;

struct PropertySelector {
index: u8,
offset: u8,
length: u8,
}

struct ComparatorEnum {
EQ: u8,
NEQ: u8,
LT: u8,
LTE: u8,
GT: u8,
GTE: u8,
}

global Comparator = ComparatorEnum {
EQ: 1,
NEQ: 2,
LT: 3,
LTE: 4,
GT: 5,
GTE: 6,
};

struct Select {
property_selector: PropertySelector,
value: Field,
Expand Down
4 changes: 2 additions & 2 deletions noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use dep::protocol_types::{
abis::function_selector::FunctionSelector, address::{AztecAddress, PartialAddress},
abis::function_selector::FunctionSelector, address::AztecAddress,
constants::CONTRACT_INSTANCE_LENGTH, contract_instance::ContractInstance
};
use crate::context::inputs::{PublicContextInputs, PrivateContextInputs};
use crate::test::helpers::utils::{Deployer, TestAccount};
use crate::test::helpers::utils::TestAccount;
use crate::keys::public_keys::PublicKeys;

unconstrained pub fn reset() {
Expand Down
84 changes: 84 additions & 0 deletions noir-projects/aztec-nr/aztec/src/utils/collapse.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use dep::protocol_types::traits::Eq;

// Collapses an array of Options with sparse Some values into a BoundedVec, essentially unwrapping the Options and
// removing the None values. For example, given:
// input: [some(3), none(), some(1)]
// this returns
// collapsed: [3, 1]
pub fn collapse_array<T, let N: u32>(input: [Option<T>; N]) -> BoundedVec<T, N> where T: Eq {
let (collapsed, collapsed_to_input_index_mapping) = unsafe {
get_collapse_hints(input)
};
verify_collapse_hints(input, collapsed, collapsed_to_input_index_mapping);
collapsed
}

pub fn verify_collapse_hints<T, let N: u32>(
input: [Option<T>; N],
collapsed: BoundedVec<T, N>,
collapsed_to_input_index_mapping: BoundedVec<u32, N>
) where T: Eq {
// collapsed should be a BoundedVec with all the non-none elements in input, in the same order. We need to lay down
// multiple constraints to guarantee this.

// First we check that the number of elements is correct
let mut count = 0;
for i in 0..N {
if input[i].is_some() {
count += 1;
}
}
assert_eq(count, collapsed.len(), "Wrong collapsed vec length");

// Then we check that all elements exist in the original array, and are in the same order. To do this we use the
// auxiliary collapsed_to_input_index_mapping array, which at index n contains the index in the input array that
// corresponds to the collapsed entry at index n.
// Example:
// - input: [some(3), none(), some(1)]
// - collapsed: [3, 1]
// - collapsed_to_input_index_mapping: [0, 2]
// These two arrays should therefore have the same length.
assert_eq(collapsed.len(), collapsed_to_input_index_mapping.len(), "Collapse hint vec length mismatch");

// We now look at each collapsed entry and check that there is a valid equal entry in the input array.
let mut last_index = Option::none();
for i in 0..N {
if i < collapsed.len() {
let input_index = collapsed_to_input_index_mapping.get_unchecked(i);
assert(input_index < N, "Out of bounds index hint");

assert_eq(collapsed.get_unchecked(i), input[input_index].unwrap(), "Wrong collapsed vec content");

// By requiring increasing input indices, we both guarantee that we're not looking at the same input
// element more than once, and that we're going over them in the original order.
if last_index.is_some() {
assert(input_index > last_index.unwrap_unchecked(), "Wrong collapsed vec order");
}
last_index = Option::some(input_index);
} else {
// BoundedVec assumes that the unused parts of the storage are zeroed out (e.g. in the Eq impl), so we make
// sure that this property holds.
assert_eq(collapsed.get_unchecked(i), std::mem::zeroed(), "Dirty collapsed vec storage");
}
}
// We now know that:
// - all values in the collapsed array exist in the input array
// - the order of the collapsed values is the same as in the input array
// - no input value is present more than once in the collapsed array
// - the number of elements in the collapsed array is the same as in the input array.
// Therefore, the collapsed array is correct.
}

unconstrained fn get_collapse_hints<T, let N: u32>(input: [Option<T>; N]) -> (BoundedVec<T, N>, BoundedVec<u32, N>) {
let mut collapsed: BoundedVec<T, N> = BoundedVec::new();
let mut collapsed_to_input_index_mapping: BoundedVec<u32, N> = BoundedVec::new();

for i in 0..N {
if input[i].is_some() {
collapsed.push(input[i].unwrap_unchecked());
collapsed_to_input_index_mapping.push(i);
}
}

(collapsed, collapsed_to_input_index_mapping)
}
Loading

0 comments on commit 138dc52

Please sign in to comment.