From 5f9546a50b72e29ec032e115a79ce5ceae2f26c0 Mon Sep 17 00:00:00 2001 From: Gregorio Juliana Date: Wed, 14 Feb 2024 14:38:09 +0100 Subject: [PATCH] feat: static call support in aztec.nr and acir-simulator (#4106) Closes https://github.com/AztecProtocol/aztec-packages/issues/4036, https://github.com/AztecProtocol/aztec-packages/issues/2872 Added support for static calls, making sure callcontext is properly forwarded and checked in the simulator before reaching the kernel circuits. --- .circleci/config.yml | 13 ++++ .../aztec-nr/authwit/src/entrypoint/app.nr | 14 +++- .../aztec-nr/authwit/src/entrypoint/fee.nr | 14 +++- .../aztec/src/context/private_context.nr | 64 ++++++++++++--- .../aztec/src/context/public_context.nr | 43 +++++++++- .../aztec/src/oracle/call_private_function.nr | 9 ++- .../oracle/enqueue_public_function_call.nr | 9 ++- .../aztec-nr/aztec/src/oracle/public_call.nr | 8 +- .../contracts/child_contract/Nargo.toml | 2 + .../contracts/child_contract/src/main.nr | 35 ++++++++- .../contracts/parent_contract/src/main.nr | 36 +++++++++ .../crates/private-kernel-lib/src/common.nr | 13 ++++ .../crates/public-kernel-lib/src/common.nr | 9 +++ .../contract/contract_function_interaction.ts | 1 - .../end-to-end/src/e2e_static_calls.test.ts | 78 +++++++++++++++++++ yarn-project/noir-contracts.js/package.json | 2 +- .../noir-protocol-circuits-types/package.json | 2 +- .../noir-protocol-circuits-types/src/index.ts | 1 - yarn-project/package.json | 2 +- .../simulator/src/acvm/deserialize.ts | 8 ++ .../simulator/src/acvm/oracle/oracle.ts | 8 +- .../simulator/src/acvm/oracle/typed_oracle.ts | 3 + .../src/client/client_execution_context.ts | 24 +++++- .../simulator/src/client/private_execution.ts | 1 - .../simulator/src/public/execution.ts | 23 ++++++ yarn-project/simulator/src/public/executor.ts | 18 ++++- .../src/public/public_execution_context.ts | 15 +++- 27 files changed, 413 insertions(+), 42 deletions(-) create mode 100644 yarn-project/end-to-end/src/e2e_static_calls.test.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index d0f6a6629be..7868a06e5da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -687,6 +687,17 @@ jobs: name: "Test" command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_nested_contract.test.ts + e2e-static-calls: + docker: + - image: aztecprotocol/alpine-build-image + resource_class: small + steps: + - *checkout + - *setup_env + - run: + name: "Test" + command: cond_spot_run_compose end-to-end 4 ./scripts/docker-compose.yml TEST=e2e_static_calls.test.ts + e2e-non-contract-account: docker: - image: aztecprotocol/alpine-build-image @@ -1308,6 +1319,7 @@ workflows: - e2e-state-vars: *e2e_test - e2e-block-building: *e2e_test - e2e-nested-contract: *e2e_test + - e2e-static-calls: *e2e_test - e2e-non-contract-account: *e2e_test - e2e-multiple-accounts-1-enc-key: *e2e_test - e2e-cli: *e2e_test @@ -1349,6 +1361,7 @@ workflows: - e2e-state-vars - e2e-block-building - e2e-nested-contract + - e2e-static-calls - e2e-non-contract-account - e2e-multiple-accounts-1-enc-key - e2e-cli diff --git a/noir-projects/aztec-nr/authwit/src/entrypoint/app.nr b/noir-projects/aztec-nr/authwit/src/entrypoint/app.nr index 49a22da9279..71936bc06c6 100644 --- a/noir-projects/aztec-nr/authwit/src/entrypoint/app.nr +++ b/noir-projects/aztec-nr/authwit/src/entrypoint/app.nr @@ -58,9 +58,19 @@ impl AppPayload { for call in self.function_calls { if !call.target_address.is_zero() { if call.is_public { - context.call_public_function_with_packed_args(call.target_address, call.function_selector, call.args_hash); + context.call_public_function_with_packed_args( + call.target_address, + call.function_selector, + call.args_hash, + false + ); } else { - let _result = context.call_private_function_with_packed_args(call.target_address, call.function_selector, call.args_hash); + let _result = context.call_private_function_with_packed_args( + call.target_address, + call.function_selector, + call.args_hash, + false + ); } } } diff --git a/noir-projects/aztec-nr/authwit/src/entrypoint/fee.nr b/noir-projects/aztec-nr/authwit/src/entrypoint/fee.nr index 318330295bc..20b07243841 100644 --- a/noir-projects/aztec-nr/authwit/src/entrypoint/fee.nr +++ b/noir-projects/aztec-nr/authwit/src/entrypoint/fee.nr @@ -54,9 +54,19 @@ impl FeePayload { for call in self.function_calls { if !call.target_address.is_zero() { if call.is_public { - context.call_public_function_with_packed_args(call.target_address, call.function_selector, call.args_hash); + context.call_public_function_with_packed_args( + call.target_address, + call.function_selector, + call.args_hash, + false + ); } else { - let _result = context.call_private_function_with_packed_args(call.target_address, call.function_selector, call.args_hash); + let _result = context.call_private_function_with_packed_args( + call.target_address, + call.function_selector, + call.args_hash, + false + ); } } } diff --git a/noir-projects/aztec-nr/aztec/src/context/private_context.nr b/noir-projects/aztec-nr/aztec/src/context/private_context.nr index 0e3d7fab979..d7f63f030a2 100644 --- a/noir-projects/aztec-nr/aztec/src/context/private_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/private_context.nr @@ -249,7 +249,18 @@ impl PrivateContext { ) -> [Field; RETURN_VALUES_LENGTH] { let args_hash = hash_args(args); assert(args_hash == arguments::pack_arguments(args)); - self.call_private_function_with_packed_args(contract_address, function_selector, args_hash) + self.call_private_function_with_packed_args(contract_address, function_selector, args_hash, false) + } + + pub fn call_private_function_static( + &mut self, + contract_address: AztecAddress, + function_selector: FunctionSelector, + args: [Field; ARGS_COUNT] + ) -> [Field; RETURN_VALUES_LENGTH] { + let args_hash = hash_args(args); + assert(args_hash == arguments::pack_arguments(args)); + self.call_private_function_with_packed_args(contract_address, function_selector, args_hash, true) } pub fn call_private_function_no_args( @@ -257,20 +268,30 @@ impl PrivateContext { contract_address: AztecAddress, function_selector: FunctionSelector ) -> [Field; RETURN_VALUES_LENGTH] { - self.call_private_function_with_packed_args(contract_address, function_selector, 0) + self.call_private_function_with_packed_args(contract_address, function_selector, 0, false) + } + + pub fn call_private_function_no_args_static( + &mut self, + contract_address: AztecAddress, + function_selector: FunctionSelector + ) -> [Field; RETURN_VALUES_LENGTH] { + self.call_private_function_with_packed_args(contract_address, function_selector, 0, true) } - pub fn call_private_function_with_packed_args( + fn call_private_function_with_packed_args( &mut self, contract_address: AztecAddress, function_selector: FunctionSelector, - args_hash: Field + args_hash: Field, + is_static_call: bool ) -> [Field; RETURN_VALUES_LENGTH] { let item = call_private_function_internal( contract_address, function_selector, args_hash, - self.side_effect_counter + self.side_effect_counter, + is_static_call ); assert_eq(item.public_inputs.call_context.start_side_effect_counter, self.side_effect_counter); @@ -286,7 +307,7 @@ impl PrivateContext { // the msg_sender in the nested call to be equal to our address, and the execution context address // for the nested call to be equal to the address we actually called. assert(item.public_inputs.call_context.is_delegate_call == false); - assert(item.public_inputs.call_context.is_static_call == false); + assert(item.public_inputs.call_context.is_static_call == is_static_call); assert(item.public_inputs.call_context.is_contract_deployment == false); assert( item.public_inputs.call_context.msg_sender.eq(self.inputs.call_context.storage_contract_address) @@ -306,7 +327,18 @@ impl PrivateContext { ) { let args_hash = hash_args(args); assert(args_hash == arguments::pack_arguments(args)); - self.call_public_function_with_packed_args(contract_address, function_selector, args_hash) + self.call_public_function_with_packed_args(contract_address, function_selector, args_hash, false) + } + + pub fn call_public_function_static( + &mut self, + contract_address: AztecAddress, + function_selector: FunctionSelector, + args: [Field; ARGS_COUNT] + ) { + let args_hash = hash_args(args); + assert(args_hash == arguments::pack_arguments(args)); + self.call_public_function_with_packed_args(contract_address, function_selector, args_hash, true) } pub fn call_public_function_no_args( @@ -314,20 +346,30 @@ impl PrivateContext { contract_address: AztecAddress, function_selector: FunctionSelector ) { - self.call_public_function_with_packed_args(contract_address, function_selector, 0) + self.call_public_function_with_packed_args(contract_address, function_selector, 0, false) + } + + pub fn call_public_function_no_args_static( + &mut self, + contract_address: AztecAddress, + function_selector: FunctionSelector + ) { + self.call_public_function_with_packed_args(contract_address, function_selector, 0, true) } pub fn call_public_function_with_packed_args( &mut self, contract_address: AztecAddress, function_selector: FunctionSelector, - args_hash: Field + args_hash: Field, + is_static_call: bool ) { let fields = enqueue_public_function_call_internal( contract_address, function_selector, args_hash, - self.side_effect_counter + self.side_effect_counter, + is_static_call ); let mut reader = Reader::new(fields); @@ -370,7 +412,7 @@ impl PrivateContext { // the msg_sender in the nested call to be equal to our address, and the execution context address // for the nested call to be equal to the address we actually called. assert(item.public_inputs.call_context.is_delegate_call == false); - assert(item.public_inputs.call_context.is_static_call == false); + assert(item.public_inputs.call_context.is_static_call == is_static_call); assert(item.public_inputs.call_context.is_contract_deployment == false); assert( item.public_inputs.call_context.msg_sender.eq(self.inputs.call_context.storage_contract_address) diff --git a/noir-projects/aztec-nr/aztec/src/context/public_context.nr b/noir-projects/aztec-nr/aztec/src/context/public_context.nr index aec62abe430..b20c0362b73 100644 --- a/noir-projects/aztec-nr/aztec/src/context/public_context.nr +++ b/noir-projects/aztec-nr/aztec/src/context/public_context.nr @@ -193,7 +193,28 @@ impl PublicContext { ) -> [Field; RETURN_VALUES_LENGTH] { let args_hash = hash_args(args); assert(args_hash == arguments::pack_arguments(args)); - call_public_function_internal(contract_address, function_selector, args_hash) + call_public_function_internal( + contract_address, + function_selector, + args_hash, + false + ) + } + + pub fn call_public_function_static( + _self: Self, + contract_address: AztecAddress, + function_selector: FunctionSelector, + args: [Field; ARGS_COUNT], + ) -> [Field; RETURN_VALUES_LENGTH] { + let args_hash = hash_args(args); + assert(args_hash == arguments::pack_arguments(args)); + call_public_function_internal( + contract_address, + function_selector, + args_hash, + true + ) } pub fn call_public_function_no_args( @@ -201,6 +222,24 @@ impl PublicContext { contract_address: AztecAddress, function_selector: FunctionSelector ) -> [Field; RETURN_VALUES_LENGTH] { - call_public_function_internal(contract_address, function_selector, 0) + call_public_function_internal( + contract_address, + function_selector, + 0, + false, + ) + } + + pub fn call_public_function_no_args_static( + _self: Self, + contract_address: AztecAddress, + function_selector: FunctionSelector, + ) -> [Field; RETURN_VALUES_LENGTH] { + call_public_function_internal( + contract_address, + function_selector, + 0, + true, + ) } } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/call_private_function.nr b/noir-projects/aztec-nr/aztec/src/oracle/call_private_function.nr index 069247c9ea3..a4b778b9f64 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/call_private_function.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/call_private_function.nr @@ -8,20 +8,23 @@ fn call_private_function_oracle( _contract_address: AztecAddress, _function_selector: FunctionSelector, _args_hash: Field, - _start_side_effect_counter: u32 + _start_side_effect_counter: u32, + _is_static_call: bool ) -> [Field; PRIVATE_CALL_STACK_ITEM_LENGTH] {} unconstrained pub fn call_private_function_internal( contract_address: AztecAddress, function_selector: FunctionSelector, args_hash: Field, - start_side_effect_counter: u32 + start_side_effect_counter: u32, + is_static_call: bool ) -> PrivateCallStackItem { let fields = call_private_function_oracle( contract_address, function_selector, args_hash, - start_side_effect_counter + start_side_effect_counter, + is_static_call ); PrivateCallStackItem::deserialize(fields) diff --git a/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr b/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr index d5bafbc28cc..7e80f5cdddd 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/enqueue_public_function_call.nr @@ -12,19 +12,22 @@ fn enqueue_public_function_call_oracle( _contract_address: AztecAddress, _function_selector: FunctionSelector, _args_hash: Field, - _side_effect_counter: u32 + _side_effect_counter: u32, + _is_static_call: bool ) -> [Field; ENQUEUE_PUBLIC_FUNCTION_CALL_RETURN_SIZE] {} unconstrained pub fn enqueue_public_function_call_internal( contract_address: AztecAddress, function_selector: FunctionSelector, args_hash: Field, - side_effect_counter: u32 + side_effect_counter: u32, + is_static_call: bool ) -> [Field; ENQUEUE_PUBLIC_FUNCTION_CALL_RETURN_SIZE] { enqueue_public_function_call_oracle( contract_address, function_selector, args_hash, - side_effect_counter + side_effect_counter, + is_static_call ) } diff --git a/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr b/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr index 26b29d95bfe..67ebff85446 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/public_call.nr @@ -4,13 +4,15 @@ use dep::protocol_types::{abis::function_selector::FunctionSelector, address::Az fn call_public_function_oracle( _contract_address: AztecAddress, _function_selector: FunctionSelector, - _args_hash: Field + _args_hash: Field, + _is_static_call: bool ) -> [Field; RETURN_VALUES_LENGTH] {} unconstrained pub fn call_public_function_internal( contract_address: AztecAddress, function_selector: FunctionSelector, - args_hash: Field + args_hash: Field, + is_static_call: bool ) -> [Field; RETURN_VALUES_LENGTH] { - call_public_function_oracle(contract_address, function_selector, args_hash) + call_public_function_oracle(contract_address, function_selector, args_hash, is_static_call) } diff --git a/noir-projects/noir-contracts/contracts/child_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/child_contract/Nargo.toml index 38047ccb17a..52ad24ae5a2 100644 --- a/noir-projects/noir-contracts/contracts/child_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/child_contract/Nargo.toml @@ -6,3 +6,5 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } +value_note = { path = "../../../aztec-nr/value-note" } + diff --git a/noir-projects/noir-contracts/contracts/child_contract/src/main.nr b/noir-projects/noir-contracts/contracts/child_contract/src/main.nr index f557d06bb50..6305b118317 100644 --- a/noir-projects/noir-contracts/contracts/child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/child_contract/src/main.nr @@ -3,13 +3,28 @@ contract Child { use dep::std::option::Option; use dep::aztec::{ - context::{PrivateContext, PublicContext, Context}, log::emit_unencrypted_log, - state_vars::public_state::PublicState + context::{PrivateContext, PublicContext, Context}, + log::emit_unencrypted_log, + state_vars::{ + public_state::PublicState, + set::Set + }, + protocol_types::{ + abis::{ + function_selector::FunctionSelector, + call_context::CallContext + }, + address::AztecAddress, + }, + note::{ + note_getter_options::NoteGetterOptions, + } }; - use dep::aztec::protocol_types::{abis::{call_context::CallContext, function_selector::FunctionSelector}, address::AztecAddress}; + use dep::value_note::value_note::ValueNote; struct Storage { current_value: PublicState, + a_private_value: Set, } #[aztec(private)] @@ -56,6 +71,20 @@ contract Child { new_value } + #[aztec(private)] + fn privateSetValue(new_value: Field, owner: AztecAddress) -> Field { + let mut note = ValueNote::new(new_value, owner); + storage.a_private_value.insert(&mut note, true); + new_value + } + + #[aztec(private)] + fn privateGetValue(amount: Field, owner: AztecAddress) -> Field { + let options = NoteGetterOptions::new().select(0, amount, Option::none()).select(1, owner.to_field(), Option::none()).set_limit(1); + let notes = storage.a_private_value.get_notes(options); + notes[0].unwrap_unchecked().value + } + // Increments `current_value` by `new_value` #[aztec(public)] fn pubIncValue(new_value: Field) -> Field { diff --git a/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr b/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr index 62df3e83018..15fca2e232d 100644 --- a/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr @@ -135,4 +135,40 @@ contract Parent { [targetContract.to_field(), targetSelector.to_field(), targetValue + 1] ); } + + #[aztec(private)] + fn privateStaticCall( + targetContract: AztecAddress, + targetSelector: FunctionSelector, + args: [Field; 2] + ) -> Field { + // Call the target private function + let return_values = context.call_private_function_static(targetContract, targetSelector, args); + + // Copy the return value from the call to this function's return values + return_values[0] + } + + // Public function to directly call another public function to the targetContract using the selector and value provided + #[aztec(public)] + fn publicStaticCall( + targetContract: AztecAddress, + targetSelector: FunctionSelector, + args: [Field; 1] + ) -> Field { + let return_values = context.call_public_function_static(targetContract, targetSelector, args); + + return_values[0] + } + + // Private function to enqueue a static call to the pubEntryPoint function of another contract, passing the target arguments provided + #[aztec(private)] + fn enqueueStaticCallToPubFunction( + targetContract: AztecAddress, + targetSelector: FunctionSelector, + args: [Field; 1] + ) { + // Call the target private function + context.call_public_function_static(targetContract, targetSelector, args); + } } diff --git a/noir-projects/noir-protocol-circuits/src/crates/private-kernel-lib/src/common.nr b/noir-projects/noir-protocol-circuits/src/crates/private-kernel-lib/src/common.nr index 6295d147116..aa2fb147256 100644 --- a/noir-projects/noir-protocol-circuits/src/crates/private-kernel-lib/src/common.nr +++ b/noir-projects/noir-protocol-circuits/src/crates/private-kernel-lib/src/common.nr @@ -121,6 +121,19 @@ fn perform_static_call_checks(private_call: PrivateCallData) { assert( is_empty_array(public_inputs.new_nullifiers), "new_nullifiers must be empty for static calls" ); + + let new_l2_to_l1_msgs_length = array_length(public_inputs.new_l2_to_l1_msgs); + assert(new_l2_to_l1_msgs_length == 0, "new_l2_to_l1_msgs must be empty for static calls"); + + // TODO: reevaluate when implementing https://github.com/AztecProtocol/aztec-packages/issues/1165 + // This 4 magical number is the minimum size of the buffer, since it has to store the total length of all the serialized logs. + assert( + public_inputs.encrypted_log_preimages_length == 4, "No encrypted logs are allowed for static calls" + ); + + assert( + public_inputs.unencrypted_log_preimages_length == 4, "No unencrypted logs are allowed for static calls" + ); } } diff --git a/noir-projects/noir-protocol-circuits/src/crates/public-kernel-lib/src/common.nr b/noir-projects/noir-protocol-circuits/src/crates/public-kernel-lib/src/common.nr index 863aada5d14..9931f4e01b4 100644 --- a/noir-projects/noir-protocol-circuits/src/crates/public-kernel-lib/src/common.nr +++ b/noir-projects/noir-protocol-circuits/src/crates/public-kernel-lib/src/common.nr @@ -82,6 +82,15 @@ fn perform_static_call_checks(public_call: PublicCallData) { assert( update_requests_length == 0, "No contract storage update requests are allowed for static calls" ); + + let new_l2_to_l1_msgs_length = array_length(public_inputs.new_l2_to_l1_msgs); + assert(new_l2_to_l1_msgs_length == 0, "new_l2_to_l1_msgs must be empty for static calls"); + + // TODO: reevaluate when implementing https://github.com/AztecProtocol/aztec-packages/issues/1165 + // This 4 magical number is the minimum size of the buffer, since it has to store the total length of all the serialized logs. + assert( + public_inputs.unencrypted_log_preimages_length == 4, "No unencrypted logs are allowed for static calls" + ); } } diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index 891ffb08a62..1e7a30d33d6 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -53,7 +53,6 @@ export class ContractFunctionInteraction extends BaseContractInteraction { /** * Returns an execution request that represents this operation. Useful as a building * block for constructing batch requests. - * @param options - An optional object containing additional configuration for the transaction. * @returns An execution request wrapped in promise. */ public request(): FunctionCall { diff --git a/yarn-project/end-to-end/src/e2e_static_calls.test.ts b/yarn-project/end-to-end/src/e2e_static_calls.test.ts new file mode 100644 index 00000000000..1d635f51f8d --- /dev/null +++ b/yarn-project/end-to-end/src/e2e_static_calls.test.ts @@ -0,0 +1,78 @@ +import { Wallet } from '@aztec/aztec.js'; +import { ChildContract, ParentContract } from '@aztec/noir-contracts.js'; + +import { setup } from './fixtures/utils.js'; + +describe('e2e_static_calls', () => { + let wallet: Wallet; + let parentContract: ParentContract; + let childContract: ChildContract; + let teardown: () => Promise; + + beforeEach(async () => { + ({ teardown, wallet } = await setup()); + }, 100_000); + + afterEach(() => teardown()); + + beforeEach(async () => { + parentContract = await ParentContract.deploy(wallet).send().deployed(); + childContract = await ChildContract.deploy(wallet).send().deployed(); + }, 100_000); + + describe('parent calls child', () => { + it('performs legal private to private static calls', async () => { + await parentContract.methods + .privateStaticCall(childContract.address, childContract.methods.privateGetValue.selector, [ + 42n, + wallet.getCompleteAddress().address, + ]) + .send() + .wait(); + }, 100_000); + + it('performs legal public to public static calls', async () => { + await parentContract.methods + .enqueueStaticCallToPubFunction(childContract.address, childContract.methods.pubGetValue.selector, [42n]) + .send() + .wait(); + }, 100_000); + + it('performs legal enqueued public static calls', async () => { + await parentContract.methods + .publicStaticCall(childContract.address, childContract.methods.pubGetValue.selector, [42n]) + .send() + .wait(); + }, 100_000); + + it('fails when performing illegal private to private static calls', async () => { + await expect( + parentContract.methods + .privateStaticCall(childContract.address, childContract.methods.privateSetValue.selector, [ + 42n, + wallet.getCompleteAddress().address, + ]) + .send() + .wait(), + ).rejects.toThrow('Static call cannot create new notes, emit L2->L1 messages or generate logs'); + }, 100_000); + + it('fails when performing illegal public to public static calls', async () => { + await expect( + parentContract.methods + .publicStaticCall(childContract.address, childContract.methods.pubSetValue.selector, [42n]) + .send() + .wait(), + ).rejects.toThrow('Static call cannot update the state, emit L2->L1 messages or generate logs'); + }, 100_000); + + it('fails when performing illegal enqueued public static calls', async () => { + await expect( + parentContract.methods + .enqueueStaticCallToPubFunction(childContract.address, childContract.methods.pubSetValue.selector, [42n]) + .send() + .wait(), + ).rejects.toThrow('Static call cannot update the state, emit L2->L1 messages or generate logs'); + }, 100_000); + }); +}); diff --git a/yarn-project/noir-contracts.js/package.json b/yarn-project/noir-contracts.js/package.json index df827fc8e0f..10195fd680b 100644 --- a/yarn-project/noir-contracts.js/package.json +++ b/yarn-project/noir-contracts.js/package.json @@ -49,4 +49,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/noir-protocol-circuits-types/package.json b/yarn-project/noir-protocol-circuits-types/package.json index eec6ca314b6..a580039ebca 100644 --- a/yarn-project/noir-protocol-circuits-types/package.json +++ b/yarn-project/noir-protocol-circuits-types/package.json @@ -57,4 +57,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/noir-protocol-circuits-types/src/index.ts b/yarn-project/noir-protocol-circuits-types/src/index.ts index 303bc90f52b..d544a9d3af9 100644 --- a/yarn-project/noir-protocol-circuits-types/src/index.ts +++ b/yarn-project/noir-protocol-circuits-types/src/index.ts @@ -121,7 +121,6 @@ export async function executeInner( const params: InnerInputType = { input: mapPrivateKernelInnerCircuitPrivateInputsToNoir(privateKernelInnerCircuitPrivateInputs), }; - const returnType = await executePrivateKernelInnerWithACVM(params); return mapPrivateKernelInnerCircuitPublicInputsFromNoir(returnType); diff --git a/yarn-project/package.json b/yarn-project/package.json index c83a0292d4b..06f7739b380 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -72,4 +72,4 @@ "@noir-lang/noir_wasm": "portal:../noir/packages/noir_wasm", "@noir-lang/noir_js": "portal:../noir/packages/noir_js" } -} \ No newline at end of file +} diff --git a/yarn-project/simulator/src/acvm/deserialize.ts b/yarn-project/simulator/src/acvm/deserialize.ts index 57a9e437fc5..6a04ec9e5b0 100644 --- a/yarn-project/simulator/src/acvm/deserialize.ts +++ b/yarn-project/simulator/src/acvm/deserialize.ts @@ -23,6 +23,14 @@ export function frToNumber(fr: Fr): number { return Number(fr.value); } +/** + * Converts a field to a boolean. + * @param fr - The field to convert. + */ +export function frToBoolean(fr: Fr): boolean { + return fr.toBigInt() === BigInt(1); +} + /** * Extracts the return fields of a given partial witness. * @param acir - The bytecode of the function. diff --git a/yarn-project/simulator/src/acvm/oracle/oracle.ts b/yarn-project/simulator/src/acvm/oracle/oracle.ts index 867a452fc87..330b762277a 100644 --- a/yarn-project/simulator/src/acvm/oracle/oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/oracle.ts @@ -7,7 +7,7 @@ import { Fr, Point } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; import { ACVMField } from '../acvm_types.js'; -import { frToNumber, fromACVMField } from '../deserialize.js'; +import { frToBoolean, frToNumber, fromACVMField } from '../deserialize.js'; import { toACVMField, toAcvmEnqueuePublicFunctionResult } from '../serialize.js'; import { acvmFieldMessageToString, oracleDebugCallToFormattedStr } from './debug.js'; import { TypedOracle } from './typed_oracle.js'; @@ -292,12 +292,14 @@ export class Oracle { [functionSelector]: ACVMField[], [argsHash]: ACVMField[], [sideffectCounter]: ACVMField[], + [isStaticCall]: ACVMField[], ): Promise { const callStackItem = await this.typedOracle.callPrivateFunction( AztecAddress.fromField(fromACVMField(contractAddress)), FunctionSelector.fromField(fromACVMField(functionSelector)), fromACVMField(argsHash), frToNumber(fromACVMField(sideffectCounter)), + frToBoolean(fromACVMField(isStaticCall)), ); return callStackItem.toFields().map(toACVMField); } @@ -306,11 +308,13 @@ export class Oracle { [contractAddress]: ACVMField[], [functionSelector]: ACVMField[], [argsHash]: ACVMField[], + [isStaticCall]: ACVMField[], ): Promise { const returnValues = await this.typedOracle.callPublicFunction( AztecAddress.fromField(fromACVMField(contractAddress)), FunctionSelector.fromField(fromACVMField(functionSelector)), fromACVMField(argsHash), + frToBoolean(fromACVMField(isStaticCall)), ); return padArrayEnd(returnValues, Fr.ZERO, RETURN_VALUES_LENGTH).map(toACVMField); } @@ -320,12 +324,14 @@ export class Oracle { [functionSelector]: ACVMField[], [argsHash]: ACVMField[], [sideffectCounter]: ACVMField[], + [isStaticCall]: ACVMField[], ) { const enqueuedRequest = await this.typedOracle.enqueuePublicFunctionCall( AztecAddress.fromString(contractAddress), FunctionSelector.fromField(fromACVMField(functionSelector)), fromACVMField(argsHash), frToNumber(fromACVMField(sideffectCounter)), + frToBoolean(fromACVMField(isStaticCall)), ); return toAcvmEnqueuePublicFunctionResult(enqueuedRequest); } diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index bd76ffad27b..ae53384f9f2 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -194,6 +194,7 @@ export abstract class TypedOracle { _functionSelector: FunctionSelector, _argsHash: Fr, _sideffectCounter: number, + _isStaticCall: boolean, ): Promise { throw new Error('Not available.'); } @@ -202,6 +203,7 @@ export abstract class TypedOracle { _targetContractAddress: AztecAddress, _functionSelector: FunctionSelector, _argsHash: Fr, + _isStaticCall: boolean, ): Promise { throw new Error('Not available.'); } @@ -211,6 +213,7 @@ export abstract class TypedOracle { _functionSelector: FunctionSelector, _argsHash: Fr, _sideffectCounter: number, + _isStaticCall: boolean, ): Promise { throw new Error('Not available.'); } diff --git a/yarn-project/simulator/src/client/client_execution_context.ts b/yarn-project/simulator/src/client/client_execution_context.ts index 6871bd89ca8..5f98af4e655 100644 --- a/yarn-project/simulator/src/client/client_execution_context.ts +++ b/yarn-project/simulator/src/client/client_execution_context.ts @@ -307,12 +307,25 @@ export class ClientExecutionContext extends ViewDataOracle { this.log(`Emitted unencrypted log: "${text.length > 100 ? text.slice(0, 100) + '...' : text}"`); } + #checkValidStaticCall(childExecutionResult: ExecutionResult) { + if ( + childExecutionResult.callStackItem.publicInputs.newCommitments.some(item => !item.isEmpty()) || + childExecutionResult.callStackItem.publicInputs.newNullifiers.some(item => !item.isEmpty()) || + childExecutionResult.callStackItem.publicInputs.newL2ToL1Msgs.some(item => !item.isZero()) || + !childExecutionResult.callStackItem.publicInputs.encryptedLogPreimagesLength.equals(new Fr(4)) || + !childExecutionResult.callStackItem.publicInputs.unencryptedLogPreimagesLength.equals(new Fr(4)) + ) { + throw new Error(`Static call cannot create new notes, emit L2->L1 messages or generate logs`); + } + } + /** * Calls a private function as a nested execution. * @param targetContractAddress - The address of the contract to call. * @param functionSelector - The function selector of the function to call. * @param argsHash - The packed arguments to pass to the function. * @param sideEffectCounter - The side effect counter at the start of the call. + * @param isStaticCall - Whether the call is a static call. * @returns The execution result. */ async callPrivateFunction( @@ -320,6 +333,7 @@ export class ClientExecutionContext extends ViewDataOracle { functionSelector: FunctionSelector, argsHash: Fr, sideEffectCounter: number, + isStaticCall: boolean, ) { this.log( `Calling private function ${this.contractAddress}:${functionSelector} from ${this.callContext.storageContractAddress}`, @@ -342,7 +356,7 @@ export class ClientExecutionContext extends ViewDataOracle { targetArtifact, sideEffectCounter, false, - false, + isStaticCall, ); const context = new ClientExecutionContext( @@ -366,6 +380,10 @@ export class ClientExecutionContext extends ViewDataOracle { targetFunctionData, ); + if (isStaticCall) { + this.#checkValidStaticCall(childExecutionResult); + } + this.nestedExecutions.push(childExecutionResult); return childExecutionResult.callStackItem; @@ -379,6 +397,7 @@ export class ClientExecutionContext extends ViewDataOracle { * @param functionSelector - The function selector of the function to call. * @param argsHash - The packed arguments to pass to the function. * @param sideEffectCounter - The side effect counter at the start of the call. + * @param isStaticCall - Whether the call is a static call. * @returns The public call stack item with the request information. */ public async enqueuePublicFunctionCall( @@ -386,6 +405,7 @@ export class ClientExecutionContext extends ViewDataOracle { functionSelector: FunctionSelector, argsHash: Fr, sideEffectCounter: number, + isStaticCall: boolean, ): Promise { const targetArtifact = await this.db.getFunctionArtifact(targetContractAddress, functionSelector); const derivedCallContext = await this.deriveCallContext( @@ -393,7 +413,7 @@ export class ClientExecutionContext extends ViewDataOracle { targetArtifact, sideEffectCounter, false, - false, + isStaticCall, ); const args = this.packedArgsCache.unpack(argsHash); const enqueuedRequest = PublicCallRequest.from({ diff --git a/yarn-project/simulator/src/client/private_execution.ts b/yarn-project/simulator/src/client/private_execution.ts index cf2e579af08..bb6198e3ad1 100644 --- a/yarn-project/simulator/src/client/private_execution.ts +++ b/yarn-project/simulator/src/client/private_execution.ts @@ -24,7 +24,6 @@ export async function executePrivateFunction( ): Promise { const functionSelector = functionData.selector; log(`Executing external function ${contractAddress}:${functionSelector}`); - const acir = Buffer.from(artifact.bytecode, 'base64'); const initialWitness = context.getInitialWitness(artifact); const acvmCallback = new Oracle(context); diff --git a/yarn-project/simulator/src/public/execution.ts b/yarn-project/simulator/src/public/execution.ts index 5ef65f73a02..c63eee6f0e8 100644 --- a/yarn-project/simulator/src/public/execution.ts +++ b/yarn-project/simulator/src/public/execution.ts @@ -135,3 +135,26 @@ function contractStorageUpdateRequestToPublicDataUpdateRequest( update.sideEffectCounter!, ); } + +/** + * Checks whether the child execution result is valid for a static call (no state modifications). + * @param executionResult - The execution result of a public function + */ + +export function checkValidStaticCall( + newCommitments: SideEffect[], + newNullifiers: SideEffectLinkedToNoteHash[], + contractStorageUpdateRequests: ContractStorageUpdateRequest[], + newL2ToL1Messages: Fr[], + unencryptedLogs: FunctionL2Logs, +) { + if ( + contractStorageUpdateRequests.length > 0 || + newCommitments.length > 0 || + newNullifiers.length > 0 || + newL2ToL1Messages.length > 0 || + unencryptedLogs.logs.length > 0 + ) { + throw new Error('Static call cannot update the state, emit L2->L1 messages or generate logs'); + } +} diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index e678e8c0628..ad15149e3f6 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -16,7 +16,7 @@ import { SideEffectCounter } from '../common/index.js'; import { PackedArgsCache } from '../common/packed_args_cache.js'; import { AcirSimulator } from '../index.js'; import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js'; -import { PublicExecution, PublicExecutionResult } from './execution.js'; +import { PublicExecution, PublicExecutionResult, checkValidStaticCall } from './execution.js'; import { PublicExecutionContext } from './public_execution_context.js'; /** @@ -124,11 +124,25 @@ export class PublicExecutor { this.commitmentsDb, ); + let executionResult; + try { - return await executePublicFunction(context, acir); + executionResult = await executePublicFunction(context, acir); } catch (err) { throw createSimulationError(err instanceof Error ? err : new Error('Unknown error during public execution')); } + + if (executionResult.execution.callContext.isStaticCall) { + checkValidStaticCall( + executionResult.newCommitments, + executionResult.newNullifiers, + executionResult.contractStorageUpdateRequests, + executionResult.newL2ToL1Messages, + executionResult.unencryptedLogs, + ); + } + + return executionResult; } /** diff --git a/yarn-project/simulator/src/public/public_execution_context.ts b/yarn-project/simulator/src/public/public_execution_context.ts index fd3dd5c97f5..819985ceaf8 100644 --- a/yarn-project/simulator/src/public/public_execution_context.ts +++ b/yarn-project/simulator/src/public/public_execution_context.ts @@ -8,7 +8,7 @@ import { createDebugLogger } from '@aztec/foundation/log'; import { TypedOracle, toACVMWitness } from '../acvm/index.js'; import { PackedArgsCache, SideEffectCounter } from '../common/index.js'; import { CommitmentsDB, PublicContractsDB, PublicStateDB } from './db.js'; -import { PublicExecution, PublicExecutionResult } from './execution.js'; +import { PublicExecution, PublicExecutionResult, checkValidStaticCall } from './execution.js'; import { executePublicFunction } from './executor.js'; import { ContractStorageActionsCollector } from './state_actions.js'; @@ -160,6 +160,7 @@ export class PublicExecutionContext extends TypedOracle { targetContractAddress: AztecAddress, functionSelector: FunctionSelector, argsHash: Fr, + isStaticCall: boolean, ) { const args = this.packedArgsCache.unpack(argsHash); this.log(`Public function call: addr=${targetContractAddress} selector=${functionSelector} args=${args.join(',')}`); @@ -184,7 +185,7 @@ export class PublicExecutionContext extends TypedOracle { functionSelector, isContractDeployment: false, isDelegateCall: false, - isStaticCall: false, + isStaticCall, startSideEffectCounter: 0, // TODO use counters in public execution }); @@ -209,6 +210,16 @@ export class PublicExecutionContext extends TypedOracle { const childExecutionResult = await executePublicFunction(context, acir); + if (isStaticCall) { + checkValidStaticCall( + childExecutionResult.newCommitments, + childExecutionResult.newNullifiers, + childExecutionResult.contractStorageUpdateRequests, + childExecutionResult.newL2ToL1Messages, + childExecutionResult.unencryptedLogs, + ); + } + this.nestedExecutions.push(childExecutionResult); this.log(`Returning from nested call: ret=${childExecutionResult.returnValues.join(', ')}`);