From 138dc52a232f20248306aa9a99cf66f0ac7ec7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Wed, 4 Sep 2024 22:53:52 +0200 Subject: [PATCH] feat: router contract (#8352) --- docs/docs/aztec/glossary/call_types.md | 22 ++- .../crowdfunding_contract.md | 30 +--- .../src/core/libraries/ConstantsGen.sol | 2 + .../aztec/src/note/note_getter/mod.nr | 28 ++-- .../aztec/src/note/note_getter/test.nr | 6 +- .../aztec/src/note/note_getter_options.nr | 19 +-- .../aztec/src/test/helpers/cheatcodes.nr | 4 +- .../aztec-nr/aztec/src/utils/collapse.nr | 84 ++++++++++ .../aztec-nr/aztec/src/utils/comparison.nr | 152 ++++++++++++++++++ noir-projects/aztec-nr/aztec/src/utils/mod.nr | 87 +--------- .../aztec-nr/aztec/src/utils/test.nr | 12 +- noir-projects/noir-contracts/Nargo.toml | 1 + .../app_subscription_contract/Nargo.toml | 1 + .../app_subscription_contract/src/main.nr | 43 ++--- .../contracts/counter_contract/src/main.nr | 2 +- .../crowdfunding_contract/Nargo.toml | 1 + .../crowdfunding_contract/src/main.nr | 30 ++-- .../contracts/lending_contract/src/main.nr | 7 +- .../contracts/router_contract/Nargo.toml | 8 + .../contracts/router_contract/src/main.nr | 43 +++++ .../contracts/router_contract/src/test.nr | 27 ++++ .../token_contract/src/test/access_control.nr | 1 - .../contracts/token_contract/src/test/burn.nr | 18 +-- .../src/test/transfer_private.nr | 2 +- .../token_contract/src/test/utils.nr | 3 +- .../crates/types/src/constants.nr | 1 + yarn-project/circuits.js/src/constants.gen.ts | 1 + .../circuits.js/src/contract/artifact_hash.ts | 1 + ...trained_function_broadcasted_event.test.ts | 2 +- .../src/fixtures/snapshot_manager.ts | 11 +- yarn-project/end-to-end/src/fixtures/utils.ts | 42 ++++- .../scripts/copy-contracts.sh | 1 + .../protocol-contracts/src/router/artifact.ts | 6 + .../src/router/index.test.ts | 17 ++ .../protocol-contracts/src/router/index.ts | 22 +++ .../pxe/src/pxe_service/create_pxe_service.ts | 2 + 36 files changed, 510 insertions(+), 229 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/utils/collapse.nr create mode 100644 noir-projects/aztec-nr/aztec/src/utils/comparison.nr create mode 100644 noir-projects/noir-contracts/contracts/router_contract/Nargo.toml create mode 100644 noir-projects/noir-contracts/contracts/router_contract/src/main.nr create mode 100644 noir-projects/noir-contracts/contracts/router_contract/src/test.nr create mode 100644 yarn-project/protocol-contracts/src/router/artifact.ts create mode 100644 yarn-project/protocol-contracts/src/router/index.test.ts create mode 100644 yarn-project/protocol-contracts/src/router/index.ts diff --git a/docs/docs/aztec/glossary/call_types.md b/docs/docs/aztec/glossary/call_types.md index 1f1b3cd8008..48303f44b81 100644 --- a/docs/docs/aztec/glossary/call_types.md +++ b/docs/docs/aztec/glossary/call_types.md @@ -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 diff --git a/docs/docs/tutorials/codealong/contract_tutorials/crowdfunding_contract.md b/docs/docs/tutorials/codealong/contract_tutorials/crowdfunding_contract.md index 198c1a3dfc2..e198b68d416 100644 --- a/docs/docs/tutorials/codealong/contract_tutorials/crowdfunding_contract.md +++ b/docs/docs/tutorials/codealong/contract_tutorials/crowdfunding_contract.md @@ -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: @@ -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! @@ -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. diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index d954c8b778f..769a77c970c 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -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; diff --git a/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr b/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr index 8170ddf8f61..7638072fac2 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_getter/mod.nr @@ -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; @@ -50,23 +51,12 @@ fn check_note_fields( 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." + ); } } @@ -135,7 +125,7 @@ fn constrain_get_notes_internal = BoundedVec::new(); // We have now collapsed the sparse array of Options into a BoundedVec. This is a more ergonomic type and also diff --git a/noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr b/noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr index 0f8c81cf66f..34fd32c8ba4 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_getter/test.nr @@ -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; diff --git a/noir-projects/aztec-nr/aztec/src/note/note_getter_options.nr b/noir-projects/aztec-nr/aztec/src/note/note_getter_options.nr index 88263962e7e..d4317f9edf0 100644 --- a/noir-projects/aztec-nr/aztec/src/note/note_getter_options.nr +++ b/noir-projects/aztec-nr/aztec/src/note/note_getter_options.nr @@ -1,6 +1,7 @@ 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, @@ -8,24 +9,6 @@ struct PropertySelector { 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, diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr index 12ba6715de8..ea5bca963c4 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr @@ -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() { diff --git a/noir-projects/aztec-nr/aztec/src/utils/collapse.nr b/noir-projects/aztec-nr/aztec/src/utils/collapse.nr new file mode 100644 index 00000000000..aaa531b945e --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/collapse.nr @@ -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(input: [Option; N]) -> BoundedVec 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( + input: [Option; N], + collapsed: BoundedVec, + collapsed_to_input_index_mapping: BoundedVec +) 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(input: [Option; N]) -> (BoundedVec, BoundedVec) { + let mut collapsed: BoundedVec = BoundedVec::new(); + let mut collapsed_to_input_index_mapping: BoundedVec = 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) +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/comparison.nr b/noir-projects/aztec-nr/aztec/src/utils/comparison.nr new file mode 100644 index 00000000000..e5a91883775 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/comparison.nr @@ -0,0 +1,152 @@ +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, +}; + +pub fn assert_comparison(lhs: Field, operation: u8, rhs: Field, error_msg: str) { + // Values are computed ahead of time because circuits evaluate all branches + let is_equal = lhs == rhs; + let is_lt = lhs.lt(rhs); + + if (operation == Comparator.EQ) { + assert(is_equal, error_msg); + } else if (operation == Comparator.NEQ) { + assert(!is_equal, error_msg); + } else if (operation == Comparator.LT) { + assert(is_lt, error_msg); + } else if (operation == Comparator.LTE) { + assert(is_lt | is_equal, error_msg); + } else if (operation == Comparator.GT) { + assert(!is_lt & !is_equal, error_msg); + } else if (operation == Comparator.GTE) { + assert(!is_lt, error_msg); + } +} + +mod test { + use super::assert_comparison; + use super::Comparator; + + #[test] + fn test_assert_comparison_happy_path() { + let lhs = 10; + let rhs = 10; + assert_comparison(lhs, Comparator.EQ, rhs, "Expected lhs to be equal to rhs"); + + let lhs = 10; + let rhs = 11; + assert_comparison(lhs, Comparator.NEQ, rhs, "Expected lhs to be not equal to rhs"); + + let lhs = 10; + let rhs = 11; + assert_comparison(lhs, Comparator.LT, rhs, "Expected lhs to be less than rhs"); + + let lhs = 10; + let rhs = 10; + assert_comparison( + lhs, + Comparator.LTE, + rhs, + "Expected lhs to be less than or equal to rhs" + ); + + let lhs = 11; + let rhs = 10; + assert_comparison(lhs, Comparator.GT, rhs, "Expected lhs to be greater than rhs"); + + let lhs = 10; + let rhs = 10; + assert_comparison( + lhs, + Comparator.GTE, + rhs, + "Expected lhs to be greater than or equal to rhs" + ); + + let lhs = 11; + let rhs = 10; + assert_comparison( + lhs, + Comparator.GTE, + rhs, + "Expected lhs to be greater than or equal to rhs" + ); + } + + #[test(should_fail_with="Expected lhs to be equal to rhs")] + fn test_assert_comparison_fail_eq() { + let lhs = 10; + let rhs = 11; + assert_comparison(lhs, Comparator.EQ, rhs, "Expected lhs to be equal to rhs"); + } + + #[test(should_fail_with="Expected lhs to be not equal to rhs")] + fn test_assert_comparison_fail_neq() { + let lhs = 10; + let rhs = 10; + assert_comparison(lhs, Comparator.NEQ, rhs, "Expected lhs to be not equal to rhs"); + } + + #[test(should_fail_with="Expected lhs to be less than rhs")] + fn test_assert_comparison_fail_lt() { + let lhs = 11; + let rhs = 10; + assert_comparison(lhs, Comparator.LT, rhs, "Expected lhs to be less than rhs"); + } + + #[test(should_fail_with="Expected lhs to be less than or equal to rhs")] + fn test_assert_comparison_fail_lte() { + let lhs = 11; + let rhs = 10; + assert_comparison( + lhs, + Comparator.LTE, + rhs, + "Expected lhs to be less than or equal to rhs" + ); + } + + #[test(should_fail_with="Expected lhs to be greater than rhs")] + fn test_assert_comparison_fail_gt() { + let lhs = 10; + let rhs = 10; + assert_comparison(lhs, Comparator.GT, rhs, "Expected lhs to be greater than rhs"); + } + + #[test(should_fail_with="Expected lhs to be greater than or equal to rhs")] + fn test_assert_comparison_fail_gte() { + let lhs = 10; + let rhs = 11; + assert_comparison( + lhs, + Comparator.GTE, + rhs, + "Expected lhs to be greater than or equal to rhs" + ); + } + + #[test(should_fail_with="Expected lhs to be greater than or equal to rhs")] + fn test_assert_comparison_fail_gte_2() { + let lhs = 10; + let rhs = 11; + assert_comparison( + lhs, + Comparator.GTE, + rhs, + "Expected lhs to be greater than or equal to rhs" + ); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/mod.nr b/noir-projects/aztec-nr/aztec/src/utils/mod.nr index 6d79c195f33..e670a6403bf 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/mod.nr @@ -1,85 +1,4 @@ -use dep::protocol_types::traits::Eq; - -mod test; +mod collapse; +mod comparison; mod point; - -// 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(input: [Option; N]) -> BoundedVec where T: Eq { - let (collapsed, collapsed_to_input_index_mapping) = get_collapse_hints(input); - verify_collapse_hints(input, collapsed, collapsed_to_input_index_mapping); - collapsed -} - -fn verify_collapse_hints( - input: [Option; N], - collapsed: BoundedVec, - collapsed_to_input_index_mapping: BoundedVec -) 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(input: [Option; N]) -> (BoundedVec, BoundedVec) { - let mut collapsed: BoundedVec = BoundedVec::new(); - let mut collapsed_to_input_index_mapping: BoundedVec = 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) -} +mod test; diff --git a/noir-projects/aztec-nr/aztec/src/utils/test.nr b/noir-projects/aztec-nr/aztec/src/utils/test.nr index 5ea5ad23624..85b827c792d 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/test.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/test.nr @@ -1,9 +1,9 @@ -use crate::utils::{collapse, verify_collapse_hints}; +use super::collapse::{collapse_array, verify_collapse_hints}; #[test] fn collapse_empty_array() { let original: [Option; 2] = [Option::none(), Option::none()]; - let collapsed = collapse(original); + let collapsed = collapse_array(original); assert_eq(collapsed.len(), 0); } @@ -11,7 +11,7 @@ fn collapse_empty_array() { #[test] fn collapse_non_sparse_array() { let original = [Option::some(7), Option::some(3), Option::none()]; - let collapsed = collapse(original); + let collapsed = collapse_array(original); assert_eq(collapsed.len(), 2); assert_eq(collapsed.get(0), 7); @@ -21,7 +21,7 @@ fn collapse_non_sparse_array() { #[test] fn collapse_sparse_array() { let original = [Option::some(7), Option::none(), Option::some(3)]; - let collapsed = collapse(original); + let collapsed = collapse_array(original); assert_eq(collapsed.len(), 2); assert_eq(collapsed.get(0), 7); @@ -31,7 +31,7 @@ fn collapse_sparse_array() { #[test] fn collapse_array_front_padding() { let original = [Option::none(), Option::none(), Option::some(7), Option::none(), Option::some(3)]; - let collapsed = collapse(original); + let collapsed = collapse_array(original); assert_eq(collapsed.len(), 2); assert_eq(collapsed.get(0), 7); @@ -41,7 +41,7 @@ fn collapse_array_front_padding() { #[test] fn collapse_array_back_padding() { let original = [Option::some(7), Option::none(), Option::some(3), Option::none(), Option::none()]; - let collapsed = collapse(original); + let collapsed = collapse_array(original); assert_eq(collapsed.len(), 2); assert_eq(collapsed.get(0), 7); diff --git a/noir-projects/noir-contracts/Nargo.toml b/noir-projects/noir-contracts/Nargo.toml index 1bb0fafff27..f3d2b637a44 100644 --- a/noir-projects/noir-contracts/Nargo.toml +++ b/noir-projects/noir-contracts/Nargo.toml @@ -32,6 +32,7 @@ members = [ "contracts/pending_note_hashes_contract", "contracts/price_feed_contract", "contracts/private_fpc_contract", + "contracts/router_contract", "contracts/schnorr_account_contract", "contracts/schnorr_hardcoded_account_contract", "contracts/schnorr_single_key_account_contract", diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/app_subscription_contract/Nargo.toml index 956f7855db0..c75ef918f92 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/Nargo.toml @@ -8,3 +8,4 @@ type = "contract" aztec = { path = "../../../aztec-nr/aztec" } authwit = { path = "../../../aztec-nr/authwit" } token = { path = "../token_contract" } +router = { path = "../router_contract" } diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr index c695fc0841f..73fd1f257bb 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr @@ -2,18 +2,17 @@ mod subscription_note; mod dapp_payload; contract AppSubscription { - use crate::{dapp_payload::DAppPayload, subscription_note::{SubscriptionNote, SUBSCRIPTION_NOTE_LEN}}; + use crate::{dapp_payload::DAppPayload, subscription_note::SubscriptionNote}; use aztec::{ - prelude::{ - AztecAddress, FunctionSelector, PrivateContext, NoteHeader, Map, PrivateMutable, PublicMutable, - SharedImmutable - }, + prelude::{AztecAddress, Map, PrivateMutable, SharedImmutable}, encrypted_logs::encrypted_note_emission::{encode_and_encrypt_note, encode_and_encrypt_note_with_keys}, - keys::getters::get_current_public_keys, protocol_types::constants::MAX_FIELD_VALUE + keys::getters::get_current_public_keys, + protocol_types::constants::{MAX_FIELD_VALUE, ROUTER_ADDRESS}, utils::comparison::Comparator }; - use authwit::{auth_witness::get_auth_witness, auth::assert_current_call_valid_authwit}; + use authwit::auth::assert_current_call_valid_authwit; use token::Token; + use router::Router; #[aztec(storage)] struct Storage { @@ -53,7 +52,9 @@ contract AppSubscription { context.end_setup(); - AppSubscription::at(context.this_address()).assert_not_expired(note.expiry_block_number).enqueue_view(&mut context); + // We check that the note is not expired. We do that via the router contract to conceal which contract + // is performing the check. + Router::at(ROUTER_ADDRESS).check_block_number(note.expiry_block_number, Comparator.GT).call(&mut context); payload.execute_calls(&mut context, storage.target_address.read_private()); } @@ -74,23 +75,6 @@ contract AppSubscription { storage.fee_juice_limit_per_tx.initialize(fee_juice_limit_per_tx); } - #[aztec(public)] - #[aztec(internal)] - #[aztec(view)] - fn assert_not_expired(expiry_block_number: Field) { - assert((context.block_number()) as u64 < expiry_block_number as u64); - } - - #[aztec(public)] - #[aztec(internal)] - #[aztec(view)] - fn assert_block_number(expiry_block_number: Field) { - assert( - (context.block_number() + SUBSCRIPTION_DURATION_IN_BLOCKS) as u64 - >= expiry_block_number as u64 - ); - } - #[aztec(private)] fn subscribe(subscriber: AztecAddress, nonce: Field, expiry_block_number: Field, tx_count: Field) { assert(tx_count as u64 <= SUBSCRIPTION_TXS as u64); @@ -102,8 +86,13 @@ contract AppSubscription { nonce ).call(&mut context); - // Assert that the given expiry_block_number < current_block_number + SUBSCRIPTION_DURATION_IN_BLOCKS. - AppSubscription::at(context.this_address()).assert_block_number(expiry_block_number).enqueue_view(&mut context); + // Assert that the `expiry_block_number - SUBSCRIPTION_DURATION_IN_BLOCKS < current_block_number`. + // --> We do that via the router contract to conceal which contract is performing the check. + Router::at(ROUTER_ADDRESS).check_block_number( + expiry_block_number - SUBSCRIPTION_DURATION_IN_BLOCKS, + Comparator.LT + ).call(&mut context); + let subscriber_keys = get_current_public_keys(&mut context, subscriber); let msg_sender_ovpk_m = get_current_public_keys(&mut context, context.msg_sender()).ovpk_m; diff --git a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr index d5e554022cf..62a3afd057d 100644 --- a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr @@ -1,7 +1,7 @@ contract Counter { // docs:start:imports use dep::aztec::prelude::{AztecAddress, Map}; - use dep::value_note::{balance_utils, value_note::{ValueNote, VALUE_NOTE_LEN}}; + use dep::value_note::{balance_utils, value_note::ValueNote}; use dep::easy_private_state::EasyPrivateUint; // docs:end:imports diff --git a/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml index aa72f2f65f4..69185126c38 100644 --- a/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/crowdfunding_contract/Nargo.toml @@ -8,3 +8,4 @@ type = "contract" aztec = { path = "../../../aztec-nr/aztec" } value_note = { path = "../../../aztec-nr/value-note" } token = { path = "../token_contract" } +router = { path = "../router_contract" } diff --git a/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr index 3c7f9b60389..2d5a06b9e8b 100644 --- a/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/crowdfunding_contract/src/main.nr @@ -4,16 +4,17 @@ contract Crowdfunding { // docs:start:all-deps use dep::aztec::{ - protocol_types::address::AztecAddress, + protocol_types::{address::AztecAddress, constants::ROUTER_ADDRESS}, encrypted_logs::encrypted_note_emission::encode_and_encrypt_note_with_keys, - keys::getters::get_current_public_keys, - state_vars::{PrivateSet, PublicImmutable, SharedImmutable} + keys::getters::get_current_public_keys, state_vars::{PrivateSet, SharedImmutable}, + note::note_getter_options::Comparator }; use dep::aztec::unencrypted_logs::unencrypted_event_emission::encode_event; // docs:start:import_valuenote use dep::value_note::value_note::ValueNote; // docs:end:import_valuenote use dep::token::Token; + use router::Router; // docs:end:all-deps #[aztec(event)] @@ -30,12 +31,13 @@ contract Crowdfunding { // Crowdfunding campaign operator operator: SharedImmutable, // End of the crowdfunding campaign after which no more donations are accepted - deadline: PublicImmutable, + deadline: SharedImmutable, // Notes emitted to donors when they donate (can be used as proof to obtain rewards, eg in Claim contracts) donation_receipts: PrivateSet, } // docs:end:storage + // TODO(#8367): Ensure deadline is quantized to improve privacy set. // docs:start:init // docs:start:init-header // docs:start:init-header-error @@ -51,24 +53,14 @@ contract Crowdfunding { } // docs:end:init - // docs:start:deadline - // docs:start:deadline-header - #[aztec(public)] - #[aztec(internal)] - #[aztec(view)] - fn _check_deadline() { - // docs:end:deadline-header - let deadline = storage.deadline.read(); - assert(context.timestamp() < deadline, "Deadline has passed"); - } - // docs:end:deadline - // docs:start:donate - // docs:start:call-check-deadline #[aztec(private)] fn donate(amount: u64) { - // 1) Check that the deadline has not passed - Crowdfunding::at(context.this_address())._check_deadline().enqueue_view(&mut context); + // 1) Check that the deadline has not passed --> we do that via the router contract to conceal which contract + // is performing the check. + // docs:start:call-check-deadline + let deadline = storage.deadline.read_private(); + Router::at(ROUTER_ADDRESS).check_timestamp(deadline, Comparator.GT).call(&mut context); // docs:end:call-check-deadline // docs:start:do-transfer diff --git a/noir-projects/noir-contracts/contracts/lending_contract/src/main.nr b/noir-projects/noir-contracts/contracts/lending_contract/src/main.nr index 2a7eac34721..93d7052e876 100644 --- a/noir-projects/noir-contracts/contracts/lending_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/lending_contract/src/main.nr @@ -11,13 +11,12 @@ mod helpers; // - A way to repay all debt at once // - Liquidations contract Lending { - use dep::aztec::prelude::{FunctionSelector, AztecAddress, PrivateContext, Map, PublicMutable}; - use dep::aztec::context::{PublicContext, gas::GasOpts}; + use dep::aztec::prelude::{AztecAddress, Map, PublicMutable}; use crate::asset::Asset; use crate::position::Position; use crate::interest_math::compute_multiplier; - use crate::helpers::{covered_by_collateral, DebtReturn, debt_updates, debt_value, compute_identifier}; + use crate::helpers::{covered_by_collateral, debt_updates, debt_value, compute_identifier}; use dep::token::Token; use dep::price_feed::PriceFeed; @@ -81,7 +80,7 @@ contract Lending { // accumulator *= multiplier, and multiplier >= 1 asset.interest_accumulator = (asset.interest_accumulator * multiplier) / precision; - asset.last_updated_ts = context.timestamp(); + asset.last_updated_ts = timestamp; asset_loc.write(asset); } diff --git a/noir-projects/noir-contracts/contracts/router_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/router_contract/Nargo.toml new file mode 100644 index 00000000000..f1e292f4cc5 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/router_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "router_contract" +authors = [""] +compiler_version = ">=0.25.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } diff --git a/noir-projects/noir-contracts/contracts/router_contract/src/main.nr b/noir-projects/noir-contracts/contracts/router_contract/src/main.nr new file mode 100644 index 00000000000..484a701b7bb --- /dev/null +++ b/noir-projects/noir-contracts/contracts/router_contract/src/main.nr @@ -0,0 +1,43 @@ +mod test; + +/// The purpose of this contract is to perform a check in public without revealing what contract enqued the public +/// call. This is achieved by having a private function on this contract that enques the public call and hence +/// the `msg_sender` in the public call is the address of this contract. +contract Router { + use aztec::utils::comparison::assert_comparison; + + // docs:start:check_timestamp + /// Enqueues a public call that asserts that `lhs` (left side of the comparison) timestamp satisfies + /// the `operation` with respect to the current timestamp. + #[aztec(private)] + fn check_timestamp(lhs: u64, operation: u8) { + Router::at(context.this_address())._check_timestamp(lhs, operation).enqueue_view(&mut context); + } + + #[aztec(public)] + #[aztec(internal)] + #[aztec(view)] + fn _check_timestamp(lhs: u64, operation: u8) { + let lhs_field = lhs as Field; + let rhs = context.timestamp() as Field; + assert_comparison(lhs_field, operation, rhs, "Timestamp mismatch."); + } + // docs:end:check_timestamp + + /// Enqueues a public call that asserts that `lhs` (left side of the comparison) block number satisfies + /// the `operation` with respect to the current block number. + #[aztec(private)] + fn check_block_number(lhs: Field, operation: u8) { + // docs:start:enqueueing + Router::at(context.this_address())._check_block_number(lhs, operation).enqueue_view(&mut context); + // docs:end:enqueueing + } + + #[aztec(public)] + #[aztec(internal)] + #[aztec(view)] + fn _check_block_number(lhs: Field, operation: u8) { + let rhs = context.block_number(); + assert_comparison(lhs, operation, rhs, "Block number mismatch."); + } +} diff --git a/noir-projects/noir-contracts/contracts/router_contract/src/test.nr b/noir-projects/noir-contracts/contracts/router_contract/src/test.nr new file mode 100644 index 00000000000..992d719425e --- /dev/null +++ b/noir-projects/noir-contracts/contracts/router_contract/src/test.nr @@ -0,0 +1,27 @@ +use dep::aztec::test::helpers::test_environment::TestEnvironment; +use crate::Router; +use aztec::utils::comparison::Comparator; + +#[test] +unconstrained fn test_check_block_number() { + let mut env = TestEnvironment::new(); + + let router_contract = env.deploy_self("Router").without_initializer(); + let router_contract_address = router_contract.to_address(); + let router = Router::at(router_contract_address); + + env.advance_block_by(9); + + // First we sanity-check that current block number is as expected + let current_block_number = env.block_number(); + assert(current_block_number == 10, "Expected block number to be 10"); + + // We test just one success case and 1 failure case in this test as the rest is tested in the comparator unit tests + let call_1 = router.check_block_number(11, Comparator.GT); + env.call_private_void(call_1); + + let call_2 = router.check_block_number(5, Comparator.GT); + env.assert_private_call_fails(call_2); +} + +// TODO(#8372): Add test for check_timestamp --> setting timestamp currently not supported by TXE \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr index 7e83b3c16fe..45a4ce6a295 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr @@ -1,5 +1,4 @@ use crate::test::utils; -use dep::aztec::test::helpers::cheatcodes; use crate::Token; #[test] diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr index a0a2c08a492..99d71bf0bd9 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr @@ -1,5 +1,5 @@ use crate::test::utils; -use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand}; +use dep::aztec::oracle::unsafe_rand::unsafe_rand; use dep::authwit::cheatcodes as authwit_cheatcodes; use crate::Token; @@ -8,7 +8,7 @@ unconstrained fn burn_public_success() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); let burn_amount = mint_amount / 10; - // Burn less than balance + // Burn less than balance let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, 0); env.call_public(burn_call_interface); utils::check_public_balance(token_contract_address, owner, mint_amount - burn_amount); @@ -33,7 +33,7 @@ unconstrained fn burn_public_on_behalf_of_other() { unconstrained fn burn_public_failure_more_than_balance() { let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); - // Burn more than balance + // Burn more than balance let burn_amount = mint_amount * 10; let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, 0); env.assert_public_call_fails(burn_call_interface); @@ -91,7 +91,7 @@ unconstrained fn burn_private_on_behalf_of_self() { let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); let burn_amount = mint_amount / 10; - // Burn less than balance + // Burn less than balance let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, 0); env.call_private_void(burn_call_interface); utils::check_private_balance(token_contract_address, owner, mint_amount - burn_amount); @@ -116,7 +116,7 @@ unconstrained fn burn_private_on_behalf_of_other() { unconstrained fn burn_private_failure_more_than_balance() { let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); - // Burn more than balance + // Burn more than balance let burn_amount = mint_amount * 10; let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, 0); env.call_private_void(burn_call_interface); @@ -127,7 +127,7 @@ unconstrained fn burn_private_failure_more_than_balance() { unconstrained fn burn_private_failure_on_behalf_of_self_non_zero_nonce() { let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); - // Burn more than balance + // Burn more than balance let burn_amount = mint_amount / 10; let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); env.call_private_void(burn_call_interface); @@ -138,7 +138,7 @@ unconstrained fn burn_private_failure_on_behalf_of_self_non_zero_nonce() { unconstrained fn burn_private_failure_on_behalf_of_other_more_than_balance() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); - // Burn more than balance + // Burn more than balance let burn_amount = mint_amount * 10; // Burn on behalf of other let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); @@ -153,7 +153,7 @@ unconstrained fn burn_private_failure_on_behalf_of_other_more_than_balance() { unconstrained fn burn_private_failure_on_behalf_of_other_without_approval() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); - // Burn more than balance + // Burn more than balance let burn_amount = mint_amount / 10; // Burn on behalf of other let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); @@ -167,7 +167,7 @@ unconstrained fn burn_private_failure_on_behalf_of_other_without_approval() { unconstrained fn burn_private_failure_on_behalf_of_other_wrong_designated_caller() { let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); - // Burn more than balance + // Burn more than balance let burn_amount = mint_amount / 10; // Burn on behalf of other let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr index 79ab994aeb5..0b4c9cb72d6 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr @@ -1,5 +1,5 @@ use crate::test::utils; -use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, protocol_types::address::AztecAddress}; +use dep::aztec::test::helpers::cheatcodes; use dep::authwit::cheatcodes as authwit_cheatcodes; use crate::Token; diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr index abbac962303..fde110352eb 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr @@ -2,11 +2,10 @@ use dep::aztec::{ hash::compute_secret_hash, prelude::AztecAddress, test::helpers::{cheatcodes, test_environment::TestEnvironment}, protocol_types::storage::map::derive_storage_slot_in_map, - note::{note_getter::{MAX_NOTES_PER_PAGE, view_notes}, note_viewer_options::NoteViewerOptions}, oracle::{execution::{get_block_number, get_contract_address}, unsafe_rand::unsafe_rand, storage::storage_read} }; -use crate::{types::{token_note::TokenNote, transparent_note::TransparentNote}, Token}; +use crate::{types::transparent_note::TransparentNote, Token}; pub fn setup(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress) { // Setup env, generate keys diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index 68f2ed5ea3e..154bc65ecb4 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -196,6 +196,7 @@ global CANONICAL_AUTH_REGISTRY_ADDRESS = AztecAddress::from_field(0x24877c50868f global DEPLOYER_CONTRACT_ADDRESS = AztecAddress::from_field(0x2ab1a2bd6d07d8d61ea56d85861446349e52c6b7c0612b702cb1e6db6ad0b089); global REGISTERER_CONTRACT_ADDRESS = AztecAddress::from_field(0x05d15342d76e46e5be07d3cda0d753158431cdc5e39d29ce4e8fe1f5c070564a); global FEE_JUICE_ADDRESS = AztecAddress::from_field(0x16a83e3395bc921a2441db55dce24f0e0932636901a2e676fa68b9b2b9a644c1); +global ROUTER_ADDRESS = AztecAddress::from_field(0x11fc9d3c438ea027f3d52cb7cf844fa4bb197520205c7366b8887a624f6a7b2c); // LENGTH OF STRUCTS SERIALIZED TO FIELDS global AZTEC_ADDRESS_LENGTH = 1; diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index 9a55c2fee31..10056cb822c 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -126,6 +126,7 @@ export const DEPLOYER_CONTRACT_ADDRESS = 193109947607833303683371634801986023939 export const REGISTERER_CONTRACT_ADDRESS = 2631409926445785927331173506476539962589925110142857699603561302478860342858n; export const FEE_JUICE_ADDRESS = 10248142274714515101077825679585135641434041564851038865006795089686437446849n; +export const ROUTER_ADDRESS = 8135649085127523915405560812661632604783066942985338123941332115593181690668n; export const AZTEC_ADDRESS_LENGTH = 1; export const GAS_FEES_LENGTH = 2; export const GAS_LENGTH = 2; diff --git a/yarn-project/circuits.js/src/contract/artifact_hash.ts b/yarn-project/circuits.js/src/contract/artifact_hash.ts index a5d6f7673af..05fb484ca13 100644 --- a/yarn-project/circuits.js/src/contract/artifact_hash.ts +++ b/yarn-project/circuits.js/src/contract/artifact_hash.ts @@ -72,6 +72,7 @@ export function computeArtifactMetadataHash(artifact: ContractArtifact) { 'FeeJuice', 'ContractInstanceDeployer', 'ContractClassRegisterer', + 'Router', ]; // This is a temporary workaround for the canonical contracts to have deterministic deployments. diff --git a/yarn-project/circuits.js/src/contract/events/unconstrained_function_broadcasted_event.test.ts b/yarn-project/circuits.js/src/contract/events/unconstrained_function_broadcasted_event.test.ts index d331a41b1cf..33cd679e0c5 100644 --- a/yarn-project/circuits.js/src/contract/events/unconstrained_function_broadcasted_event.test.ts +++ b/yarn-project/circuits.js/src/contract/events/unconstrained_function_broadcasted_event.test.ts @@ -18,7 +18,7 @@ describe('UnconstrainedFunctionBroadcastedEvent', () => { expect(event).toMatchSnapshot(); }); - it('filters out zero-elements at the end of the artifcat tree sibling path', () => { + it('filters out zero-elements at the end of the artifact tree sibling path', () => { const siblingPath: Tuple = [Fr.ZERO, new Fr(1), Fr.ZERO, new Fr(2), Fr.ZERO]; const event = new UnconstrainedFunctionBroadcastedEvent( Fr.random(), diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index dac9a054a04..f462b270171 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -38,7 +38,12 @@ import { MNEMONIC } from './fixtures.js'; import { getACVMConfig } from './get_acvm_config.js'; import { getBBConfig } from './get_bb_config.js'; import { setupL1Contracts } from './setup_l1_contracts.js'; -import { deployCanonicalAuthRegistry, deployCanonicalKeyRegistry, getPrivateKeyFromIndex } from './utils.js'; +import { + deployCanonicalAuthRegistry, + deployCanonicalKeyRegistry, + deployCanonicalRouter, + getPrivateKeyFromIndex, +} from './utils.js'; import { Watcher } from './watcher.js'; export type SubsystemsContext = { @@ -350,6 +355,10 @@ async function setupFromFresh( await deployCanonicalAuthRegistry( new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(aztecNodeConfig.l1ChainId, aztecNodeConfig.version)), ); + logger.verbose('Deploying router...'); + await deployCanonicalRouter( + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(aztecNodeConfig.l1ChainId, aztecNodeConfig.version)), + ); if (statePath) { writeFileSync(`${statePath}/aztec_node_config.json`, JSON.stringify(aztecNodeConfig)); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 17ca30c3300..52322c1d86c 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -35,6 +35,7 @@ import { type EthAddress, GasSettings, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, + ROUTER_ADDRESS, computeContractAddressFromInstance, getContractClassFromArtifact, } from '@aztec/circuits.js'; @@ -57,12 +58,13 @@ import { RollupAbi, RollupBytecode, } from '@aztec/l1-artifacts'; -import { AuthRegistryContract, KeyRegistryContract } from '@aztec/noir-contracts.js'; +import { AuthRegistryContract, KeyRegistryContract, RouterContract } from '@aztec/noir-contracts.js'; import { FeeJuiceContract } from '@aztec/noir-contracts.js/FeeJuice'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types'; import { getCanonicalAuthRegistry } from '@aztec/protocol-contracts/auth-registry'; import { FeeJuiceAddress, getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import { getCanonicalKeyRegistry } from '@aztec/protocol-contracts/key-registry'; +import { getCanonicalRouter } from '@aztec/protocol-contracts/router'; import { PXEService, type PXEServiceConfig, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { type SequencerClient } from '@aztec/sequencer-client'; import { createAndStartTelemetryClient, getConfigEnvVars as getTelemetryConfig } from '@aztec/telemetry-client/start'; @@ -448,6 +450,11 @@ export async function setup( new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(config.l1ChainId, config.version)), ); } + + logger.verbose('Deploying router...'); + await deployCanonicalRouter( + new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(config.l1ChainId, config.version)), + ); } const watcher = new Watcher( @@ -789,6 +796,39 @@ export async function deployCanonicalAuthRegistry(deployer: Wallet) { await expect(deployer.getContractInstance(canonicalAuthRegistry.instance.address)).resolves.toBeDefined(); } +export async function deployCanonicalRouter(deployer: Wallet) { + const canonicalRouter = getCanonicalRouter(); + + // We check to see if there exists a contract at the Router address with the same contract class id as we expect. This means that + // the router has already been deployed to the correct address. + if ( + (await deployer.getContractInstance(canonicalRouter.address))?.contractClassId.equals( + canonicalRouter.contractClass.id, + ) && + (await deployer.isContractClassPubliclyRegistered(canonicalRouter.contractClass.id)) + ) { + return; + } + + const router = await RouterContract.deploy(deployer) + .send({ contractAddressSalt: canonicalRouter.instance.salt, universalDeploy: true }) + .deployed(); + + if ( + !router.address.equals(canonicalRouter.address) || + !router.address.equals(AztecAddress.fromBigInt(ROUTER_ADDRESS)) + ) { + throw new Error( + `Deployed Router address ${router.address} does not match expected address ${canonicalRouter.address}, or they both do not equal ROUTER_ADDRESS`, + ); + } + + expect(computeContractAddressFromInstance(router.instance)).toEqual(router.address); + expect(getContractClassFromArtifact(router.artifact).id).toEqual(router.instance.contractClassId); + await expect(deployer.isContractClassPubliclyRegistered(canonicalRouter.contractClass.id)).resolves.toBe(true); + await expect(deployer.getContractInstance(canonicalRouter.instance.address)).resolves.toBeDefined(); +} + export async function waitForProvenChain(node: AztecNode, targetBlock?: number, timeoutSec = 60, intervalSec = 1) { targetBlock ??= await node.getBlockNumber(); diff --git a/yarn-project/protocol-contracts/scripts/copy-contracts.sh b/yarn-project/protocol-contracts/scripts/copy-contracts.sh index d9f4a60a25c..3646ec63131 100755 --- a/yarn-project/protocol-contracts/scripts/copy-contracts.sh +++ b/yarn-project/protocol-contracts/scripts/copy-contracts.sh @@ -9,6 +9,7 @@ contracts=( key_registry_contract-KeyRegistry auth_registry_contract-AuthRegistry multi_call_entrypoint_contract-MultiCallEntrypoint + router_contract-Router ) diff --git a/yarn-project/protocol-contracts/src/router/artifact.ts b/yarn-project/protocol-contracts/src/router/artifact.ts new file mode 100644 index 00000000000..ca6a86b860e --- /dev/null +++ b/yarn-project/protocol-contracts/src/router/artifact.ts @@ -0,0 +1,6 @@ +import { loadContractArtifact } from '@aztec/types/abi'; +import { type NoirCompiledContract } from '@aztec/types/noir'; + +import RouterJson from '../../artifacts/Router.json' assert { type: 'json' }; + +export const RouterArtifact = loadContractArtifact(RouterJson as NoirCompiledContract); diff --git a/yarn-project/protocol-contracts/src/router/index.test.ts b/yarn-project/protocol-contracts/src/router/index.test.ts new file mode 100644 index 00000000000..2591ede3f73 --- /dev/null +++ b/yarn-project/protocol-contracts/src/router/index.test.ts @@ -0,0 +1,17 @@ +import { + AztecAddress, + ROUTER_ADDRESS, + computeContractAddressFromInstance, + getContractClassFromArtifact, +} from '@aztec/circuits.js'; + +import { getCanonicalRouter } from './index.js'; + +describe('Router', () => { + it('returns canonical protocol contract', () => { + const contract = getCanonicalRouter(); + expect(computeContractAddressFromInstance(contract.instance)).toEqual(contract.address); + expect(getContractClassFromArtifact(contract.artifact).id).toEqual(contract.contractClass.id); + expect(contract.address).toEqual(AztecAddress.fromBigInt(ROUTER_ADDRESS)); + }); +}); diff --git a/yarn-project/protocol-contracts/src/router/index.ts b/yarn-project/protocol-contracts/src/router/index.ts new file mode 100644 index 00000000000..46b94433ba9 --- /dev/null +++ b/yarn-project/protocol-contracts/src/router/index.ts @@ -0,0 +1,22 @@ +import { AztecAddress, ROUTER_ADDRESS } from '@aztec/circuits.js'; + +import { type ProtocolContract, getCanonicalProtocolContract } from '../protocol_contract.js'; +import { RouterArtifact } from './artifact.js'; + +/** Returns the canonical deployment of the router. */ +export function getCanonicalRouter(): ProtocolContract { + const contract = getCanonicalProtocolContract(RouterArtifact, 1); + + if (!contract.address.equals(RouterAddress)) { + throw new Error( + `Incorrect address for router (got ${contract.address.toString()} but expected ${RouterAddress.toString()}). Check ROUTER_ADDRESS is set to the correct value in the constants files and run the protocol-contracts package tests.`, + ); + } + return contract; +} + +export function getCanonicalRouterAddress(): AztecAddress { + return getCanonicalRouter().address; +} + +export const RouterAddress = AztecAddress.fromBigInt(ROUTER_ADDRESS); diff --git a/yarn-project/pxe/src/pxe_service/create_pxe_service.ts b/yarn-project/pxe/src/pxe_service/create_pxe_service.ts index e50e14f5339..fd4b1d3a34d 100644 --- a/yarn-project/pxe/src/pxe_service/create_pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/create_pxe_service.ts @@ -10,6 +10,7 @@ import { getCanonicalFeeJuice } from '@aztec/protocol-contracts/fee-juice'; import { getCanonicalInstanceDeployer } from '@aztec/protocol-contracts/instance-deployer'; import { getCanonicalKeyRegistry } from '@aztec/protocol-contracts/key-registry'; import { getCanonicalMultiCallEntrypointContract } from '@aztec/protocol-contracts/multi-call-entrypoint'; +import { getCanonicalRouter } from '@aztec/protocol-contracts/router'; import { type PXEServiceConfig } from '../config/index.js'; import { KVPxeDatabase } from '../database/kv_pxe_database.js'; @@ -52,6 +53,7 @@ export async function createPXEService( getCanonicalFeeJuice(), getCanonicalKeyRegistry(), getCanonicalAuthRegistry(), + getCanonicalRouter(), ]) { await server.registerContract(contract); }