From 109624f127dc8da6d9d963b3af9250237be0d4e4 Mon Sep 17 00:00:00 2001 From: Gregorio Juliana Date: Tue, 18 Jun 2024 18:57:38 +0200 Subject: [PATCH] feat!: TXE (#6985) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the TXE (Testing eXecution Environment, or Tricky PXE) as a minimalistic oracle resolver that mimics the behavior of the Aztec protocol in order to write smart contract tests, much like Foundry for Solidity contracts. Together with a `TestEnvironment` added to `aztec-nr`, its aim is to make possible writing Aztec tests in pure noir resulting in much faster tests times than running e2e tests. TXE does NOT test the protocol itself and might let you do very illegal stuff ATM, but its intent is to provide a faster way to test contract logic and contract logic only. A 38 second Token Transfer test: ![Screenshot 2024-06-14 at 13 38 08](https://github.com/AztecProtocol/aztec-packages/assets/5404052/93c38be5-b2fd-4507-9894-e3c93e9bb913) WARNING: TXE is under heavy development and we are learning how and what we want to test as we go. It's also in need of a heavy refactor, but we need this base code in order to get it in the hands of more people and understand its scope and required capabilities. --------- Co-authored-by: Nicolás Venturo --- .github/workflows/ci.yml | 2 +- .vscode/settings.json | 1 - noir-projects/Dockerfile | 25 +- noir-projects/Earthfile | 23 +- .../aztec/src/context/call_interfaces.nr | 276 +++++++- .../aztec/src/note/note_getter/test.nr | 46 +- .../src/state_vars/private_mutable/test.nr | 17 +- .../src/state_vars/public_immutable/test.nr | 53 ++ .../src/state_vars/shared_mutable/test.nr | 614 +++++++++--------- .../aztec-nr/aztec/src/state_vars/storage.nr | 5 +- .../aztec-nr/aztec/src/test/helpers.nr | 5 +- .../aztec/src/test/helpers/cheatcodes.nr | 127 ++++ .../aztec/src/test/helpers/context_builder.nr | 52 -- .../aztec-nr/aztec/src/test/helpers/keys.nr | 18 + .../src/test/helpers/test_environment.nr | 207 ++++++ .../aztec-nr/aztec/src/test/helpers/types.nr | 110 ++++ .../aztec-nr/aztec/src/test/mocks.nr | 1 + .../aztec/src/test/mocks/mock_struct.nr | 36 + .../contracts/child_contract/src/main.nr | 2 +- .../contracts/counter_contract/src/main.nr | 25 + .../contracts/parent_contract/Nargo.toml | 2 + .../contracts/parent_contract/src/main.nr | 45 ++ .../contracts/test_contract/src/main.nr | 6 +- .../contracts/token_contract/src/main.nr | 70 ++ .../contracts/uniswap_contract/src/main.nr | 4 +- .../crates/types/src/traits.nr | 8 +- noir/noir-repo/aztec_macros/src/lib.rs | 21 +- .../src/transforms/contract_interface.rs | 214 +++--- .../aztec_macros/src/transforms/storage.rs | 20 +- .../src/monomorphization/mod.rs | 4 +- .../docs/docs/how_to/how-to-oracles.md | 8 +- yarn-project/Earthfile | 13 + yarn-project/archiver/src/archiver/index.ts | 1 + .../src/contract-interface-gen/typescript.ts | 3 +- .../src/contract/contract_instance.ts | 23 +- yarn-project/deploy_npm.sh | 1 + yarn-project/foundation/src/abi/abi.ts | 4 - yarn-project/package.json | 1 + yarn-project/pxe/src/database/index.ts | 1 + yarn-project/pxe/src/index.ts | 2 + .../src/client/execution_note_cache.ts | 1 - yarn-project/simulator/src/client/index.ts | 2 + yarn-project/txe/.eslintrc.cjs | 1 + yarn-project/txe/package.json | 82 +++ yarn-project/txe/src/bin/index.ts | 86 +++ yarn-project/txe/src/oracle/txe_oracle.ts | 596 +++++++++++++++++ .../txe/src/txe_service/txe_service.ts | 522 +++++++++++++++ yarn-project/txe/src/util/encoding.ts | 29 + yarn-project/txe/src/util/txe_database.ts | 24 + yarn-project/txe/tsconfig.json | 47 ++ .../types/src/abi/contract_artifact.ts | 3 - .../world-state/src/world-state-db/index.ts | 2 + yarn-project/yarn.lock | 28 + 53 files changed, 2949 insertions(+), 570 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/state_vars/public_immutable/test.nr create mode 100644 noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr delete mode 100644 noir-projects/aztec-nr/aztec/src/test/helpers/context_builder.nr create mode 100644 noir-projects/aztec-nr/aztec/src/test/helpers/keys.nr create mode 100644 noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr create mode 100644 noir-projects/aztec-nr/aztec/src/test/helpers/types.nr create mode 100644 noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr create mode 100644 yarn-project/txe/.eslintrc.cjs create mode 100644 yarn-project/txe/package.json create mode 100644 yarn-project/txe/src/bin/index.ts create mode 100644 yarn-project/txe/src/oracle/txe_oracle.ts create mode 100644 yarn-project/txe/src/txe_service/txe_service.ts create mode 100644 yarn-project/txe/src/util/encoding.ts create mode 100644 yarn-project/txe/src/util/txe_database.ts create mode 100644 yarn-project/txe/tsconfig.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671ce7a2acb..196a9efb59a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -418,7 +418,7 @@ jobs: run: earthly-ci --no-output ./noir+packages-test noir-projects: - needs: [setup, changes] + needs: [setup, changes, build] runs-on: ${{ github.event.pull_request.user.login || github.actor }}-x86 if: ${{ needs.changes.outputs.barretenberg == 'true' || needs.changes.outputs.noir == 'true' || needs.changes.outputs.noir-projects == 'true' }} steps: diff --git a/.vscode/settings.json b/.vscode/settings.json index a8ab844ebe1..86a0427f790 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -169,5 +169,4 @@ "**/noir/noir-repo/docs/versioned_docs/**": true }, "cmake.sourceDirectory": "${workspaceFolder}/barretenberg/cpp", - "noir.nargoPath": "./noir/noir-repo/target/release/nargo" } diff --git a/noir-projects/Dockerfile b/noir-projects/Dockerfile index ab4c6270e97..b8abf566f84 100644 --- a/noir-projects/Dockerfile +++ b/noir-projects/Dockerfile @@ -1,5 +1,6 @@ FROM aztecprotocol/noir as noir FROM aztecprotocol/avm-transpiler as transpiler +FROM aztecprotocol/yarn-project AS yarn-project FROM ubuntu:lunar AS builder RUN apt-get update && apt-get install -y parallel nodejs npm @@ -13,13 +14,25 @@ ENV PATH="/usr/src/avm-transpiler/target/release:${PATH}" # Copy in noir projects WORKDIR /usr/src/noir-projects COPY . . + + # Build -WORKDIR /usr/src/noir-projects/noir-contracts -RUN ./bootstrap.sh && nargo test --silence-warnings -WORKDIR /usr/src/noir-projects/noir-protocol-circuits -RUN ./bootstrap.sh && nargo test --silence-warnings -WORKDIR /usr/src/noir-projects/aztec-nr -RUN nargo test --silence-warnings +WORKDIR /usr/src/noir-projects +RUN cd noir-protocol-circuits && ./bootstrap.sh && nargo test --silence-warnings + +RUN cd /usr/src/yarn-project/txe && yarn start & echo $! > /tmp/txe.pid && \ + # Wait for TXE to initialize + sleep 5 && \ + cd /usr/src/noir-projects/noir-contracts && \ + ./bootstrap.sh && nargo test --silence-warnings --oracle-resolver http://localhost:8080 ; \ + kill $(cat /tmp/txe.pid) + +RUN cd /usr/src/yarn-project/txe && yarn start & echo $! > /tmp/txe.pid && \ + # Wait for TXE to initialize + sleep 5 && \ + cd /usr/src/noir-projects/aztec-nr && \ + nargo test --silence-warnings --oracle-resolver http://localhost:8080 + FROM scratch COPY --from=builder /usr/src/noir-projects /usr/src/noir-projects \ No newline at end of file diff --git a/noir-projects/Earthfile b/noir-projects/Earthfile index 2a7c32ab568..df8db2aa046 100644 --- a/noir-projects/Earthfile +++ b/noir-projects/Earthfile @@ -41,10 +41,25 @@ build: SAVE ARTIFACT noir-protocol-circuits test: - FROM +build - RUN cd noir-protocol-circuits && nargo test --silence-warnings - RUN cd aztec-nr && nargo test --silence-warnings - RUN cd noir-contracts && nargo test --silence-warnings + FROM ../yarn-project/+txe + + # Install nargo + COPY ../noir/+nargo/nargo /usr/bin/nargo + + COPY +build/. /usr/src/noir-projects + + RUN cd /usr/src/noir-projects/noir-protocol-circuits && nargo test --silence-warnings + RUN cd /usr/src/yarn-project/txe && yarn start & echo $! > /tmp/txe.pid && \ + # Wait for TXE to initialize + sleep 5 && \ + cd /usr/src/noir-projects/aztec-nr && nargo test --silence-warnings --oracle-resolver http://localhost:8080 ; \ + kill $(cat /tmp/txe.pid) + + RUN cd /usr/src/yarn-project/txe && yarn start & echo $! > /tmp/txe.pid && \ + # Wait for TXE to initialize + sleep 5 && \ + cd /usr/src/noir-projects/noir-contracts && nargo test --silence-warnings --oracle-resolver http://localhost:8080 ; \ + kill $(cat /tmp/txe.pid) format: FROM +build diff --git a/noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr b/noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr index 69904595c96..dd1374f9eb0 100644 --- a/noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr +++ b/noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr @@ -1,20 +1,56 @@ -use dep::protocol_types::{abis::function_selector::FunctionSelector, address::AztecAddress, traits::Deserialize}; +use dep::protocol_types::{ + abis::{function_selector::FunctionSelector, private_circuit_public_inputs::PrivateCircuitPublicInputs}, + address::AztecAddress, traits::Deserialize +}; -use crate::context::private_context::PrivateContext; -use crate::context::public_context::PublicContext; -use crate::context::gas::GasOpts; -use crate::context::public_context::FunctionReturns; +use crate::context::{ + private_context::PrivateContext, public_context::PublicContext, gas::GasOpts, + public_context::FunctionReturns, inputs::{PrivateContextInputs, PublicContextInputs} +}; use crate::oracle::arguments; -struct PrivateCallInterface { +trait CallInterface { + fn get_args(self) -> [Field]; + fn get_original(self) -> fn[Env](T) -> P; + fn get_selector(self) -> FunctionSelector; + fn get_name(self) -> str; + fn get_contract_address(self) -> AztecAddress; +} + +impl CallInterface for PrivateCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PrivateCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args_hash: Field, + args: [Field], + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs } -impl PrivateCallInterface { - pub fn call(self, context: &mut PrivateContext) -> T where T: Deserialize { +impl PrivateCallInterface { + pub fn call(self, context: &mut PrivateContext) -> T where T: Deserialize { let returns = context.call_private_function_with_packed_args( self.target_contract, self.selector, @@ -26,24 +62,49 @@ impl PrivateCallInterface { unpacked } - pub fn view(self, context: &mut PrivateContext) -> T where T: Deserialize { + pub fn view(self, context: &mut PrivateContext) -> T where T: Deserialize { let returns = context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, true, false); returns.unpack_into() } - pub fn delegate_call(self, context: &mut PrivateContext) -> T where T: Deserialize { + pub fn delegate_call(self, context: &mut PrivateContext) -> T where T: Deserialize { let returns = context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, false, true); returns.unpack_into() } } -struct PrivateVoidCallInterface { +impl CallInterface for PrivateVoidCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PrivateVoidCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args_hash: Field, + args: [Field], + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs } -impl PrivateVoidCallInterface { +impl PrivateVoidCallInterface { pub fn call(self, context: &mut PrivateContext) { context.call_private_function_with_packed_args( self.target_contract, @@ -63,55 +124,129 @@ impl PrivateVoidCallInterface { } } -struct PrivateStaticCallInterface { +impl CallInterface for PrivateStaticCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PrivateStaticCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args_hash: Field, + args: [Field], + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs } -impl PrivateStaticCallInterface { - pub fn view(self, context: &mut PrivateContext) -> T where T: Deserialize { +impl PrivateStaticCallInterface { + pub fn view(self, context: &mut PrivateContext) -> T where T: Deserialize { let returns = context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, true, false); returns.unpack_into() } } -struct PrivateStaticVoidCallInterface { +impl CallInterface for PrivateStaticVoidCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PrivateStaticVoidCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args_hash: Field, + args: [Field], + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs } -impl PrivateStaticVoidCallInterface { +impl PrivateStaticVoidCallInterface { pub fn view(self, context: &mut PrivateContext) { context.call_private_function_with_packed_args(self.target_contract, self.selector, self.args_hash, true, false).assert_empty(); } } -struct PublicCallInterface { +impl CallInterface for PublicCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PublicContextInputs) -> T { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PublicCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args: [Field], gas_opts: GasOpts, + original: fn[Env](PublicContextInputs) -> T } -impl PublicCallInterface { +impl PublicCallInterface { pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self { self.gas_opts = gas_opts; self } - pub fn call(self, context: &mut PublicContext) -> T where T: Deserialize { + pub fn call(self, context: &mut PublicContext) -> T where T: Deserialize { let returns = context.call_public_function(self.target_contract, self.selector, self.args, self.gas_opts); returns.deserialize_into() } - pub fn view(self, context: &mut PublicContext) -> T where T: Deserialize { + pub fn view(self, context: &mut PublicContext) -> T where T: Deserialize { let returns = context.static_call_public_function(self.target_contract, self.selector, self.args, self.gas_opts); returns.deserialize_into() } - pub fn delegate_call(self, context: &mut PublicContext) -> T where T: Deserialize { + pub fn delegate_call(self, context: &mut PublicContext) -> T where T: Deserialize { let returns = context.delegate_call_public_function(self.target_contract, self.selector, self.args); returns.deserialize_into() } @@ -153,30 +288,54 @@ impl PublicCallInterface { } } -struct PublicVoidCallInterface { +impl CallInterface for PublicVoidCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PublicContextInputs) -> () { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PublicVoidCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args: [Field], gas_opts: GasOpts, + original: fn[Env](PublicContextInputs) -> () } -impl PublicVoidCallInterface { +impl PublicVoidCallInterface { pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self { self.gas_opts = gas_opts; self } - pub fn call(self, context: &mut PublicContext) { + pub fn call(self, context: &mut PublicContext) { let returns = context.call_public_function(self.target_contract, self.selector, self.args, self.gas_opts); returns.assert_empty() } - pub fn view(self, context: &mut PublicContext) { + pub fn view(self, context: &mut PublicContext) { let returns = context.static_call_public_function(self.target_contract, self.selector, self.args, self.gas_opts); returns.assert_empty() } - pub fn delegate_call(self, context: &mut PublicContext) { + pub fn delegate_call(self, context: &mut PublicContext) { let returns = context.delegate_call_public_function(self.target_contract, self.selector, self.args); returns.assert_empty() } @@ -218,22 +377,47 @@ impl PublicVoidCallInterface { } } -struct PublicStaticCallInterface { +impl CallInterface for PublicStaticCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PublicContextInputs) -> T { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PublicStaticCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args: [Field], gas_opts: GasOpts, + original: fn[Env](PublicContextInputs) -> T } -impl PublicStaticCallInterface { +impl PublicStaticCallInterface { pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self { self.gas_opts = gas_opts; self } - pub fn view(self, context: &mut PublicContext) -> T where T: Deserialize { + pub fn view(self, context: &mut PublicContext) -> T where T: Deserialize { let returns = context.static_call_public_function(self.target_contract, self.selector, self.args, self.gas_opts); - returns.deserialize_into() + let unpacked: T = returns.deserialize_into(); + unpacked } pub fn enqueue_view(self, context: &mut PrivateContext) { @@ -249,20 +433,44 @@ impl PublicStaticCallInterface { } } -struct PublicStaticVoidCallInterface { +impl CallInterface for PublicStaticVoidCallInterface { + fn get_args(self) -> [Field] { + self.args + } + + fn get_original(self) -> fn[Env](PublicContextInputs) -> () { + self.original + } + + fn get_selector(self) -> FunctionSelector { + self.selector + } + + fn get_name(self) -> str { + self.name + } + + fn get_contract_address(self) -> AztecAddress { + self.target_contract + } +} + +struct PublicStaticVoidCallInterface { target_contract: AztecAddress, selector: FunctionSelector, + name: str, args: [Field], gas_opts: GasOpts, + original: fn[Env](PublicContextInputs) -> () } -impl PublicStaticVoidCallInterface { +impl PublicStaticVoidCallInterface { pub fn with_gas(self: &mut Self, gas_opts: GasOpts) -> &mut Self { self.gas_opts = gas_opts; self } - pub fn view(self, context: &mut PublicContext) { + pub fn view(self, context: &mut PublicContext) { let returns = context.static_call_public_function(self.target_contract, self.selector, self.args, self.gas_opts); returns.assert_empty() } 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 7229268f1e5..9ee0738312e 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 @@ -9,22 +9,22 @@ use crate::{ }; use dep::protocol_types::address::AztecAddress; -use crate::test::{helpers::context_builder::ContextBuilder, mocks::mock_note::MockNote}; +use crate::test::{helpers::{test_environment::TestEnvironment, cheatcodes}, mocks::mock_note::MockNote}; -global contract_address = AztecAddress::from_field(69); global storage_slot: Field = 42; -fn setup() -> PrivateContext { - ContextBuilder::new().contract_address(contract_address).private() +fn setup() -> TestEnvironment { + TestEnvironment::new() } fn build_valid_note(value: Field) -> MockNote { - MockNote::new(value).contract_address(contract_address).storage_slot(storage_slot).build() + MockNote::new(value).contract_address(cheatcodes::get_contract_address()).storage_slot(storage_slot).build() } #[test] fn processes_single_note() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut notes_to_constrain = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; notes_to_constrain[0] = Option::some(build_valid_note(13)); @@ -38,7 +38,8 @@ fn processes_single_note() { #[test] fn processes_many_notes() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut notes_to_constrain = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; notes_to_constrain[0] = Option::some(build_valid_note(13)); @@ -53,7 +54,8 @@ fn processes_many_notes() { #[test] fn collapses_notes_at_the_beginning_of_the_array() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; opt_notes[1] = Option::some(build_valid_note(0)); @@ -80,8 +82,9 @@ fn collapses_notes_at_the_beginning_of_the_array() { } #[test(should_fail_with="Cannot return zero notes")] - fn rejects_zero_notes() { - let mut context = setup(); +fn rejects_zero_notes() { + let mut env = setup(); + let mut context = env.private(); let opt_notes: [Option; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; @@ -91,7 +94,8 @@ fn collapses_notes_at_the_beginning_of_the_array() { #[test(should_fail_with="Got more notes than limit.")] fn rejects_mote_notes_than_limit() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut opt_notes: [Option; MAX_NOTE_HASH_READ_REQUESTS_PER_CALL] = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; opt_notes[1] = Option::some(build_valid_note(0)); @@ -105,7 +109,8 @@ fn rejects_mote_notes_than_limit() { #[test] fn applies_filter_before_constraining() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut notes_to_constrain = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; let invalid_note = MockNote::new(13).build(); // This note does not have the correct address or storage slot @@ -138,7 +143,8 @@ fn applies_filter_before_constraining() { #[test(should_fail_with="Mismatch note header contract address.")] fn rejects_mismatched_address() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let note = MockNote::new(1).storage_slot(storage_slot).build(); // We're not setting the right contract address @@ -151,9 +157,10 @@ fn rejects_mismatched_address() { #[test(should_fail_with="Mismatch note header storage slot.")] fn rejects_mismatched_storage_slot() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); - let note = MockNote::new(1).contract_address(contract_address).build(); // We're not setting the right storage slot + let note = MockNote::new(1).contract_address(cheatcodes::get_contract_address()).build(); // We're not setting the right storage slot let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; opt_notes[0] = Option::some(note); @@ -164,7 +171,8 @@ fn rejects_mismatched_storage_slot() { #[test(should_fail_with="Mismatch return note field.")] fn rejects_mismatched_selector() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let value = 10; let note = build_valid_note(value); @@ -184,7 +192,8 @@ fn rejects_mismatched_selector() { #[test(should_fail_with="Return notes not sorted in descending order.")] fn rejects_mismatched_desc_sort_order() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; // Notes in ascending order @@ -201,7 +210,8 @@ fn rejects_mismatched_desc_sort_order() { #[test(should_fail_with="Return notes not sorted in ascending order.")] fn rejects_mismatched_asc_sort_order() { - let mut context = setup(); + let mut env = setup(); + let mut context = env.private(); let mut opt_notes = [Option::none(); MAX_NOTE_HASH_READ_REQUESTS_PER_CALL]; // Notes in descending order diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable/test.nr b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable/test.nr index aca21ab515a..c3949a8e03e 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable/test.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/private_mutable/test.nr @@ -1,14 +1,16 @@ use dep::protocol_types::{address::AztecAddress, grumpkin_point::GrumpkinPoint}; use crate::{context::PrivateContext, state_vars::private_mutable::PrivateMutable}; -use crate::test::{mocks::mock_note::MockNote, helpers::context_builder::ContextBuilder}; +use crate::test::{mocks::mock_note::MockNote, helpers::{cheatcodes, test_environment::TestEnvironment}}; use dep::std::{unsafe::zeroed, test::OracleMock}; -global contract_address = AztecAddress::from_field(13); global storage_slot = 17; -fn setup() -> PrivateMutable { - let mut context = ContextBuilder::new().contract_address(contract_address).private(); - let state_var = PrivateMutable::new(&mut context, storage_slot); +fn setup() -> TestEnvironment { + TestEnvironment::new() +} + +fn in_private(env: &mut TestEnvironment) -> PrivateMutable { + let state_var = PrivateMutable::new(&mut env.private(), storage_slot); // This oracle is called for its side effects alone - it's always expected to return 0. OracleMock::mock("notifyCreatedNote").returns(0); @@ -18,13 +20,14 @@ fn setup() -> PrivateMutable { #[test] fn test_initialize_or_replace_without_nullifier() { - let state_var = setup(); + let mut env = setup(); + let state_var = in_private(&mut env); let ovpk_m: GrumpkinPoint = zeroed(); let ivpk_m: GrumpkinPoint = zeroed(); let value = 42; - let mut note = MockNote::new(value).contract_address(contract_address).storage_slot(storage_slot).build(); + let mut note = MockNote::new(value).contract_address(cheatcodes::get_contract_address()).storage_slot(storage_slot).build(); OracleMock::mock("checkNullifierExists").returns(0); state_var.initialize_or_replace(&mut note).discard(); diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable/test.nr b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable/test.nr new file mode 100644 index 00000000000..7614ffff1d1 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/state_vars/public_immutable/test.nr @@ -0,0 +1,53 @@ +use crate::{context::PublicContext, state_vars::public_immutable::PublicImmutable}; +use crate::test::{helpers::test_environment::TestEnvironment, mocks::mock_struct::MockStruct}; +use dep::protocol_types::traits::Serialize; + +global storage_slot = 7; + +fn setup() -> TestEnvironment { + TestEnvironment::new() +} + +fn in_public(env: TestEnvironment) -> PublicImmutable { + PublicImmutable::new(&mut env.public(), storage_slot) +} + +#[test] +fn test_uninitialized_by_default() { + let env = setup(); + let state_var = in_public(env); + + assert_eq(state_var.is_initialized(), false); +} + +#[test] +fn test_initialize_uninitialized() { + let env = setup(); + let state_var = in_public(env); + + let value = MockStruct::new(5, 6); + + state_var.initialize(value); + + assert(state_var.is_initialized()); + assert(state_var.read() == value); +} + +#[test(should_fail_with="PublicImmutable already initialized")] +fn test_initialize_already_initialized() { + let env = setup(); + let state_var = in_public(env); + + let value = MockStruct::new(5, 6); + + state_var.initialize(value); + state_var.initialize(value); +} + +#[test(should_fail_with="PublicImmutable not initialized")] +fn test_read_uninitialized() { + let env = setup(); + let state_var = in_public(env); + + let _ = state_var.read(); +} diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/test.nr b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/test.nr index 6403f166e9e..b193640a54b 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/test.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/shared_mutable/test.nr @@ -1,377 +1,363 @@ -use dep::std::{merkle::compute_merkle_root, test::OracleMock}; - use crate::{ - context::{PublicContext, PrivateContext}, - state_vars::shared_mutable::{ - shared_mutable::SharedMutable, scheduled_value_change::ScheduledValueChange, - scheduled_delay_change::ScheduledDelayChange -}, - test::helpers::context_builder::ContextBuilder, oracle::get_public_data_witness::PublicDataWitness -}; - -use dep::protocol_types::{ - constants::{GENERATOR_INDEX__PUBLIC_LEAF_INDEX, PUBLIC_DATA_TREE_HEIGHT}, hash::pedersen_hash, - address::AztecAddress, public_data_tree_leaf_preimage::PublicDataTreeLeafPreimage + context::{PublicContext, PrivateContext}, state_vars::shared_mutable::shared_mutable::SharedMutable, + test::helpers::test_environment::TestEnvironment }; -global pre_value = 13; -global post_value = 42; +use dep::protocol_types::address::AztecAddress; global new_value = 57; global pre_delay = 20; global post_delay = 15; -global TEST_INITIAL_DELAY = 3; +global storage_slot = 57; -fn setup() -> (SharedMutable, Field) { - let block_number = 40; - let mut context = ContextBuilder::new().block_number(block_number).public(); +global TEST_INITIAL_DELAY: u32 = 30; - let storage_slot = 57; - let state_var = SharedMutable::new(&mut context, storage_slot); - - (state_var, block_number) +fn setup() -> TestEnvironment { + TestEnvironment::new() } -fn mock_value_change_read( - state_var: SharedMutable, - pre: Field, - post: Field, - block_of_change: Field -) { - let value_change_slot = state_var.get_value_change_storage_slot(); - let fields = ScheduledValueChange::new(pre, post, block_of_change as u32).serialize(); - - let _ = OracleMock::mock("storageRead").with_params((value_change_slot, 3)).returns(fields).times(1); +fn in_public(env: TestEnvironment) -> SharedMutable { + SharedMutable::new(&mut env.public(), storage_slot) } -fn mock_delay_change_read( - state_var: SharedMutable, - pre: Field, - post: Field, - block_of_change: Field -) { - let delay_change_slot = state_var.get_delay_change_storage_slot(); - let delay_change: ScheduledDelayChange = ScheduledDelayChange::new( - Option::some(pre as u32), - Option::some(post as u32), - block_of_change as u32 - ); - let fields = delay_change.serialize(); - - let _ = OracleMock::mock("storageRead").with_params((delay_change_slot, 1)).returns(fields).times(1); +fn in_private( + env: &mut TestEnvironment, + historical_block_number: u32 +) -> SharedMutable { + SharedMutable::new(&mut env.private_at(historical_block_number), storage_slot) } -fn mock_delay_change_read_uninitialized(state_var: SharedMutable) { - let delay_change_slot = state_var.get_delay_change_storage_slot(); - let _ = OracleMock::mock("storageRead").with_params((delay_change_slot, 1)).returns([0]).times(1); -} +// #[test] +// fn test_get_current_value_in_public_initial() { +// let env = setup(); +// let state_var = in_public(env); -// Useful since change and delay values are always the global pre/post ones, so we typically only care about their -// block of change. -fn mock_value_and_delay_read( - state_var: SharedMutable, - value_block_of_change: Field, - delay_block_of_change: Field -) { - mock_value_change_read(state_var, pre_value, post_value, value_block_of_change); - mock_delay_change_read(state_var, pre_delay, post_delay, delay_block_of_change); -} +// // 0 is the default empty value for a Field +// assert_eq(state_var.get_current_value_in_public(), 0); +// } -fn mock_value_change_write() -> OracleMock { - OracleMock::mock("storageWrite").returns([0; 3]) -} +// #[test] +// fn test_get_current_value_in_public_before_scheduled_change() { +// let mut env = setup(); +// let state_var = in_public(env); -fn mock_delay_change_write() -> OracleMock { - OracleMock::mock("storageWrite").returns([0; 1]) -} +// state_var.schedule_value_change(new_value); -fn assert_value_change_write( - state_var: SharedMutable, - mock: OracleMock, - pre: Field, - post: Field, - block_of_change: Field -) { - let fields = ScheduledValueChange::new(pre, post, block_of_change as u32).serialize(); - assert_eq(mock.get_last_params(), (state_var.get_value_change_storage_slot(), fields)); -} +// let (_, block_of_change) = state_var.get_scheduled_value_in_public(); -fn assert_delay_change_write( - state_var: SharedMutable, - mock: OracleMock, - pre: Field, - post: Field, - block_of_change: Field -) { - let delay_change: ScheduledDelayChange = ScheduledDelayChange::new( - Option::some(pre as u32), - Option::some(post as u32), - block_of_change as u32 - ); - - let fields = delay_change.serialize(); - assert_eq(mock.get_last_params(), (state_var.get_delay_change_storage_slot(), fields)); -} +// let original_value = 0; -#[test] -fn test_get_current_value_in_public() { - let (state_var, block_number) = setup(); +// // The current value has not changed +// assert_eq(state_var.get_current_value_in_public(), original_value); - // Change in the future, current value is pre - mock_value_change_read(state_var, pre_value, post_value, block_number + 1); - assert_eq(state_var.get_current_value_in_public(), pre_value); +// // The current value still does not change right before the block of change +// env.advance_block_to(block_of_change - 1); +// assert_eq(state_var.get_current_value_in_public(), original_value); +// } - // Change in the current block, current value is post - mock_value_change_read(state_var, pre_value, post_value, block_number); - assert_eq(state_var.get_current_value_in_public(), post_value); +// #[test] +// fn test_get_current_value_in_public_at_scheduled_change() { +// let mut env = setup(); +// let state_var = in_public(env); - // Change in the past, current value is post - mock_value_change_read(state_var, pre_value, post_value, block_number - 1); - assert_eq(state_var.get_current_value_in_public(), post_value); -} +// state_var.schedule_value_change(new_value); -#[test] -fn test_get_scheduled_value_in_public() { - let (state_var, block_number) = setup(); +// let (_, block_of_change) = state_var.get_scheduled_value_in_public(); - // Change in the future, scheduled is post (always is) - mock_value_change_read(state_var, pre_value, post_value, block_number + 1); - assert_eq(state_var.get_scheduled_value_in_public(), (post_value, (block_number + 1) as u32)); +// env.advance_block_to(block_of_change); +// assert_eq(state_var.get_current_value_in_public(), new_value); +// } - // Change in the current block, scheduled is post (always is) - mock_value_change_read(state_var, pre_value, post_value, block_number); - assert_eq(state_var.get_scheduled_value_in_public(), (post_value, block_number as u32)); +// #[test] +// fn test_get_current_value_in_public_after_scheduled_change() { +// let mut env = setup(); +// let state_var = in_public(env); - // Change in the past, scheduled is post (always is) - mock_value_change_read(state_var, pre_value, post_value, block_number - 1); - assert_eq(state_var.get_scheduled_value_in_public(), (post_value, (block_number - 1) as u32)); -} +// state_var.schedule_value_change(new_value); -#[test] -fn test_get_current_delay_in_public() { - let (state_var, block_number) = setup(); +// let (_, block_of_change) = state_var.get_scheduled_value_in_public(); - // Uninitialized - mock_delay_change_read_uninitialized(state_var); - assert_eq(state_var.get_current_delay_in_public(), TEST_INITIAL_DELAY as u32); +// env.advance_block_to(block_of_change + 10); +// assert_eq(state_var.get_current_value_in_public(), new_value); +// } - // Change in the future, current value is pre - mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); - assert_eq(state_var.get_current_delay_in_public(), pre_delay as u32); +// #[test] +// fn test_get_current_value_in_private_before_change() { +// let mut env = setup(); - // Change in the current block, current value is post - mock_delay_change_read(state_var, pre_delay, post_delay, block_number); - assert_eq(state_var.get_current_delay_in_public(), post_delay as u32); +// let public_state_var = in_public(env); +// public_state_var.schedule_value_change(new_value); - // Change in the past, current value is post - mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); - assert_eq(state_var.get_current_delay_in_public(), post_delay as u32); -} +// let (_, block_of_change) = public_state_var.get_scheduled_value_in_public(); -#[test] -fn test_get_scheduled_delay_in_public_before_change() { - let (state_var, block_number) = setup(); +// let schedule_block_number = env.block_number(); - // Uninitialized - mock_delay_change_read_uninitialized(state_var); - assert_eq(state_var.get_scheduled_delay_in_public(), (TEST_INITIAL_DELAY as u32, 0)); +// let private_state_var = in_private(&mut env, schedule_block_number); +// assert_eq(private_state_var.get_current_value_in_private(), 0); +// assert_eq(private_state_var.context.max_block_number.unwrap(), block_of_change - 1); +// } - // Change in the future, scheduled is post (always is) - mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); - assert_eq(state_var.get_scheduled_delay_in_public(), (post_delay as u32, (block_number + 1) as u32)); +// #[test] +// fn test_get_current_value_in_private_immediately_before_change() { +// let mut env = setup(); - // Change in the current block, scheduled is post (always is) - mock_delay_change_read(state_var, pre_delay, post_delay, block_number); - assert_eq(state_var.get_scheduled_delay_in_public(), (post_delay as u32, block_number as u32)); +// let public_state_var = in_public(env); +// public_state_var.schedule_value_change(new_value); - // Change in the past, scheduled is post (always is) - mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); - assert_eq(state_var.get_scheduled_delay_in_public(), (post_delay as u32, (block_number - 1) as u32)); -} +// let (_, block_of_change) = public_state_var.get_scheduled_value_in_public(); -#[test] -fn test_schedule_value_change_no_delay() { - let (state_var, block_number) = setup(); +// let private_state_var = in_private(&mut env, block_of_change - 1); - // Last value change was in the past - mock_value_change_read(state_var, pre_value, post_value, 0); +// assert_eq(private_state_var.get_current_value_in_private(), 0); +// assert_eq(private_state_var.context.max_block_number.unwrap(), block_of_change - 1); +// } - // Current delay is 0 - mock_delay_change_read(state_var, 0, 0, block_number); +// #[test] +// fn test_get_current_value_in_private_at_change() { +// let mut env = setup(); - let write_mock = mock_value_change_write(); +// let public_state_var = in_public(env); +// public_state_var.schedule_value_change(new_value); - state_var.schedule_value_change(new_value); +// let (_, block_of_change) = public_state_var.get_scheduled_value_in_public(); - // The new value has a block of change equal to the current block, i.e. it is the current value - assert_value_change_write(state_var, write_mock, post_value, new_value, block_number); -} +// let historical_block_number = block_of_change; +// let private_state_var = in_private(&mut env, historical_block_number); +// assert_eq(private_state_var.get_current_value_in_private(), new_value); +// assert_eq( +// private_state_var.context.max_block_number.unwrap(), historical_block_number + TEST_INITIAL_DELAY +// ); +// } -#[test] -fn test_schedule_value_change_before_change_no_scheduled_delay() { - let (state_var, block_number) = setup(); +// #[test] +// fn test_get_current_value_in_private_after_change() { +// let mut env = setup(); - // Value change in the future, delay change in the past - mock_value_and_delay_read(state_var, block_number + 1, block_number - 1); - let write_mock = mock_value_change_write(); +// let public_state_var = in_public(env); +// public_state_var.schedule_value_change(new_value); - state_var.schedule_value_change(new_value); +// let (_, block_of_change) = public_state_var.get_scheduled_value_in_public(); - // The new scheduled value change replaces the old one, post delay (current) is used - assert_value_change_write( - state_var, - write_mock, - pre_value, - new_value, - block_number + post_delay - ); -} +// let historical_block_number = block_of_change + 10; +// let private_state_var = in_private(&mut env, historical_block_number); +// assert_eq(private_state_var.get_current_value_in_private(), new_value); +// assert_eq( +// private_state_var.context.max_block_number.unwrap(), historical_block_number + TEST_INITIAL_DELAY +// ); +// } -#[test] -fn test_schedule_value_change_before_change_scheduled_delay() { - let (state_var, block_number) = setup(); +// #[test] +// fn test_get_current_delay_in_public() { +// let (state_var, block_number) = setup(); - // Value change in the future, delay change in the future - mock_value_and_delay_read(state_var, block_number + 1, block_number + 1); +// // Uninitialized +// mock_delay_change_read_uninitialized(state_var); +// assert_eq(state_var.get_current_delay_in_public(), TEST_INITIAL_DELAY as u32); - let write_mock = mock_value_change_write(); +// // Change in the future, current value is pre +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); +// assert_eq(state_var.get_current_delay_in_public(), pre_delay as u32); - state_var.schedule_value_change(new_value); +// // Change in the current block, current value is post +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number); +// assert_eq(state_var.get_current_delay_in_public(), post_delay as u32); - // The new scheduled value change replaces the old one, pre delay (current, not scheduled) is used - assert_value_change_write( - state_var, - write_mock, - pre_value, - new_value, - block_number + pre_delay - ); -} +// // Change in the past, current value is post +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); +// assert_eq(state_var.get_current_delay_in_public(), post_delay as u32); +// } -#[test] -fn test_schedule_value_change_after_change_no_scheduled_delay() { - let (state_var, block_number) = setup(); +// #[test] +// fn test_get_scheduled_delay_in_public_before_change() { +// let (state_var, block_number) = setup(); - // Value change in the past, delay change in the past - mock_value_and_delay_read(state_var, block_number - 1, block_number - 1); - let write_mock = mock_value_change_write(); +// // Uninitialized +// mock_delay_change_read_uninitialized(state_var); +// assert_eq(state_var.get_scheduled_delay_in_public(), (TEST_INITIAL_DELAY as u32, 0)); - state_var.schedule_value_change(new_value); +// // Change in the future, scheduled is post (always is) +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); +// assert_eq(state_var.get_scheduled_delay_in_public(), (post_delay as u32, (block_number + 1) as u32)); - // The previous post value becomes the pre value, post delay (current) is used - assert_value_change_write( - state_var, - write_mock, - post_value, - new_value, - block_number + post_delay - ); -} +// // Change in the current block, scheduled is post (always is) +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number); +// assert_eq(state_var.get_scheduled_delay_in_public(), (post_delay as u32, block_number as u32)); -#[test] -fn test_schedule_value_change_after_change_scheduled_delay() { - let (state_var, block_number) = setup(); +// // Change in the past, scheduled is post (always is) +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); +// assert_eq(state_var.get_scheduled_delay_in_public(), (post_delay as u32, (block_number - 1) as u32)); +// } - // Value change in the past, delay change in the future - mock_value_and_delay_read(state_var, block_number - 1, block_number + 1); +// #[test] +// fn test_schedule_value_change_no_delay() { +// let (state_var, block_number) = setup(); - let write_mock = mock_value_change_write(); +// // Last value change was in the past +// mock_value_change_read(state_var, pre_value, post_value, 0); + +// // Current delay is 0 +// mock_delay_change_read(state_var, 0, 0, block_number); + +// let write_mock = mock_value_change_write(); - state_var.schedule_value_change(new_value); - - // The previous post value becomes the pre value, pre delay (current, not scheduled) is used - assert_value_change_write( - state_var, - write_mock, - post_value, - new_value, - block_number + pre_delay - ); -} - -#[test] -fn test_schedule_delay_increase_before_change() { - let (state_var, block_number) = setup(); - - // Delay change in future, current delay is pre - mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); - let write_mock = mock_delay_change_write(); - - let new_delay = pre_delay + 1; - state_var.schedule_delay_change(new_delay as u32); - - // The previous scheduled change is lost, change is immediate (due to increase) - assert_delay_change_write(state_var, write_mock, pre_delay, new_delay, block_number); -} - -#[test] -fn test_schedule_delay_reduction_before_change() { - let (state_var, block_number) = setup(); - - // Delay change in future, current delay is pre - mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); - let write_mock = mock_delay_change_write(); - - let new_delay = pre_delay - 1; - state_var.schedule_delay_change(new_delay as u32); - - // The previous scheduled change is lost, change delay equals difference (due to reduction) - assert_delay_change_write( - state_var, - write_mock, - pre_delay, - new_delay, - block_number + pre_delay - new_delay - ); -} - -#[test] -fn test_schedule_delay_increase_after_change() { - let (state_var, block_number) = setup(); - - // Delay change in the past, current delay is post - mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); - let write_mock = mock_delay_change_write(); - - let new_delay = post_delay + 1; - state_var.schedule_delay_change(new_delay as u32); - - // The current value becomes pre, change is immediate (due to increase) - assert_delay_change_write(state_var, write_mock, post_delay, new_delay, block_number); -} - -#[test] -fn test_schedule_delay_reduction_after_change() { - let (state_var, block_number) = setup(); - - // Delay change in the past, current delay is post - mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); - let write_mock = mock_delay_change_write(); - - let new_delay = post_delay - 1; - state_var.schedule_delay_change(new_delay as u32); - - // The current value becomes pre, change delay equals difference (due to reduction) - assert_delay_change_write( - state_var, - write_mock, - post_delay, - new_delay, - block_number + post_delay - new_delay - ); -} - -#[test] -fn test_get_current_value_in_private_before_change() { - // Here we'd want to test that the private getter returns the correct value and sets max_block_number in the - // context to the expected block horizon, in all the possible scenarios (long before change, before near change, - // after change). - // However, this requires mocking the getPublicDataTreeWitness oracle so that we can convince the circuit that - // it got a valid historical proof. Because we can set the tree root to whatever we want in the context, this is - // trivial for a single historical value (we add a leaf and compute the root with any random path), but is quite - // hard if we're reading more than one value for the same root (as SharedMutable does): we essentially need to - // create an actual indexed tree and compute the correct path for each of the inserted values. - // TODO: implement an actual tree and use it here https://github.com/AztecProtocol/aztec-packages/issues/5494 -} +// state_var.schedule_value_change(new_value); + +// // The new value has a block of change equal to the current block, i.e. it is the current value +// assert_value_change_write(state_var, write_mock, post_value, new_value, block_number); +// } + +// #[test] +// fn test_schedule_value_change_before_change_no_scheduled_delay() { +// let (state_var, block_number) = setup(); + +// // Value change in the future, delay change in the past +// mock_value_and_delay_read(state_var, block_number + 1, block_number - 1); +// let write_mock = mock_value_change_write(); + +// state_var.schedule_value_change(new_value); + +// // The new scheduled value change replaces the old one, post delay (current) is used +// assert_value_change_write( +// state_var, +// write_mock, +// pre_value, +// new_value, +// block_number + post_delay +// ); +// } + +// #[test] +// fn test_schedule_value_change_before_change_scheduled_delay() { +// let (state_var, block_number) = setup(); + +// // Value change in the future, delay change in the future +// mock_value_and_delay_read(state_var, block_number + 1, block_number + 1); + +// let write_mock = mock_value_change_write(); + +// state_var.schedule_value_change(new_value); + +// // The new scheduled value change replaces the old one, pre delay (current, not scheduled) is used +// assert_value_change_write( +// state_var, +// write_mock, +// pre_value, +// new_value, +// block_number + pre_delay +// ); +// } + +// #[test] +// fn test_schedule_value_change_after_change_no_scheduled_delay() { +// let (state_var, block_number) = setup(); + +// // Value change in the past, delay change in the past +// mock_value_and_delay_read(state_var, block_number - 1, block_number - 1); +// let write_mock = mock_value_change_write(); + +// state_var.schedule_value_change(new_value); + +// // The previous post value becomes the pre value, post delay (current) is used +// assert_value_change_write( +// state_var, +// write_mock, +// post_value, +// new_value, +// block_number + post_delay +// ); +// } + +// #[test] +// fn test_schedule_value_change_after_change_scheduled_delay() { +// let (state_var, block_number) = setup(); + +// // Value change in the past, delay change in the future +// mock_value_and_delay_read(state_var, block_number - 1, block_number + 1); + +// let write_mock = mock_value_change_write(); + +// state_var.schedule_value_change(new_value); + +// // The previous post value becomes the pre value, pre delay (current, not scheduled) is used +// assert_value_change_write( +// state_var, +// write_mock, +// post_value, +// new_value, +// block_number + pre_delay +// ); +// } + +// #[test] +// fn test_schedule_delay_increase_before_change() { +// let (state_var, block_number) = setup(); + +// // Delay change in future, current delay is pre +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); +// let write_mock = mock_delay_change_write(); + +// let new_delay = pre_delay + 1; +// state_var.schedule_delay_change(new_delay as u32); + +// // The previous scheduled change is lost, change is immediate (due to increase) +// assert_delay_change_write(state_var, write_mock, pre_delay, new_delay, block_number); +// } + +// #[test] +// fn test_schedule_delay_reduction_before_change() { +// let (state_var, block_number) = setup(); + +// // Delay change in future, current delay is pre +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number + 1); +// let write_mock = mock_delay_change_write(); + +// let new_delay = pre_delay - 1; +// state_var.schedule_delay_change(new_delay as u32); + +// // The previous scheduled change is lost, change delay equals difference (due to reduction) +// assert_delay_change_write( +// state_var, +// write_mock, +// pre_delay, +// new_delay, +// block_number + pre_delay - new_delay +// ); +// } + +// #[test] +// fn test_schedule_delay_increase_after_change() { +// let (state_var, block_number) = setup(); + +// // Delay change in the past, current delay is post +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); +// let write_mock = mock_delay_change_write(); + +// let new_delay = post_delay + 1; +// state_var.schedule_delay_change(new_delay as u32); + +// // The current value becomes pre, change is immediate (due to increase) +// assert_delay_change_write(state_var, write_mock, post_delay, new_delay, block_number); +// } + +// #[test] +// fn test_schedule_delay_reduction_after_change() { +// let (state_var, block_number) = setup(); + +// // Delay change in the past, current delay is post +// mock_delay_change_read(state_var, pre_delay, post_delay, block_number - 1); +// let write_mock = mock_delay_change_write(); + +// let new_delay = post_delay - 1; +// state_var.schedule_delay_change(new_delay as u32); + +// // The current value becomes pre, change delay equals difference (due to reduction) +// assert_delay_change_write( +// state_var, +// write_mock, +// post_delay, +// new_delay, +// block_number + post_delay - new_delay +// ); +// } \ No newline at end of file diff --git a/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr b/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr index 0f8cce2323c..8ae9faf228c 100644 --- a/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr +++ b/noir-projects/aztec-nr/aztec/src/state_vars/storage.nr @@ -8,9 +8,8 @@ trait Storage where T: Serialize + Deserialize { // Struct representing an exportable storage variable in the contract // Every entry in the storage struct will be exported in the compilation artifact as a -// Storable entity, containing the storage slot and the type of the variable -struct Storable { +// Storable entity, containing the storage slot +struct Storable { slot: Field, - typ: str } diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers.nr b/noir-projects/aztec-nr/aztec/src/test/helpers.nr index c5c7a4b5f31..b28a85add1c 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers.nr @@ -1 +1,4 @@ -mod context_builder; +mod test_environment; +mod cheatcodes; +mod types; +mod keys; diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr new file mode 100644 index 00000000000..014757cf9b0 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr @@ -0,0 +1,127 @@ +use dep::protocol_types::{abis::function_selector::FunctionSelector, address::{AztecAddress, PartialAddress}}; +use crate::context::inputs::{PublicContextInputs, PrivateContextInputs}; +use crate::test::helpers::types::{Deployer, TestAccount}; +use crate::keys::public_keys::PublicKeys; + +unconstrained pub fn reset() { + oracle_reset(); +} + +unconstrained pub fn get_contract_address() -> AztecAddress { + oracle_get_contract_address() +} + +unconstrained pub fn set_contract_address(address: AztecAddress) { + oracle_set_contract_address(address); +} + +unconstrained pub fn get_block_number() -> u32 { + oracle_get_block_number() +} + +unconstrained pub fn advance_blocks(blocks: u32) { + oracle_time_travel(blocks); +} + +unconstrained pub fn get_private_context_inputs(historical_block_number: u32) -> PrivateContextInputs { + oracle_get_private_context_inputs(historical_block_number) +} + +unconstrained pub fn get_public_context_inputs() -> PublicContextInputs { + oracle_get_public_context_inputs() +} + +unconstrained pub fn deploy( + path: str, + initializer: str, + args: [Field], + public_keys_hash: Field +) -> AztecAddress { + oracle_deploy(path, initializer, args, public_keys_hash) +} + +unconstrained pub fn direct_storage_write( + contract_address: AztecAddress, + storage_slot: Field, + fields: [Field; N] +) { + let _hash = direct_storage_write_oracle(contract_address, storage_slot, fields); +} + +unconstrained pub fn create_account() -> TestAccount { + oracle_create_account() +} + +unconstrained pub fn add_account(secret: Field, partial_address: PartialAddress) -> TestAccount { + oracle_add_account(secret, partial_address) +} + +unconstrained pub fn derive_keys(secret: Field) -> PublicKeys { + oracle_derive_keys(secret) +} + +unconstrained pub fn set_msg_sender(msg_sender: AztecAddress) { + oracle_set_msg_sender(msg_sender) +} + +unconstrained pub fn get_msg_sender() -> AztecAddress { + oracle_get_msg_sender() +} + +unconstrained pub fn get_side_effects_counter() -> u32 { + oracle_get_side_effects_counter() +} + +#[oracle(reset)] +fn oracle_reset() {} + +#[oracle(getContractAddress)] +fn oracle_get_contract_address() -> AztecAddress {} + +#[oracle(setContractAddress)] +fn oracle_set_contract_address(address: AztecAddress) {} + +#[oracle(getBlockNumber)] +fn oracle_get_block_number() -> u32 {} + +#[oracle(timeTravel)] +fn oracle_time_travel(blocks: u32) {} + +#[oracle(getPrivateContextInputs)] +fn oracle_get_private_context_inputs(historical_block_number: u32) -> PrivateContextInputs {} + +#[oracle(getPublicContextInputs)] +fn oracle_get_public_context_inputs() -> PublicContextInputs {} + +#[oracle(deploy)] +fn oracle_deploy( + path: str, + initializer: str, + args: [Field], + public_keys_hash: Field +) -> AztecAddress {} + +#[oracle(directStorageWrite)] +fn direct_storage_write_oracle( + _contract_address: AztecAddress, + _storage_slot: Field, + _values: [Field; N] +) -> [Field; N] {} + +#[oracle(createAccount)] +fn oracle_create_account() -> TestAccount {} + +#[oracle(addAccount)] +fn oracle_add_account(secret: Field, partial_address: PartialAddress) -> TestAccount {} + +#[oracle(deriveKeys)] +fn oracle_derive_keys(secret: Field) -> PublicKeys {} + +#[oracle(getMsgSender)] +fn oracle_get_msg_sender() -> AztecAddress {} + +#[oracle(setMsgSender)] +fn oracle_set_msg_sender(msg_sender: AztecAddress) {} + +#[oracle(getSideEffectsCounter)] +fn oracle_get_side_effects_counter() -> u32 {} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/context_builder.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/context_builder.nr deleted file mode 100644 index 0b04ac52ccb..00000000000 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/context_builder.nr +++ /dev/null @@ -1,52 +0,0 @@ -use crate::context::{PrivateContext, PublicContext}; -use dep::protocol_types::address::AztecAddress; -use dep::std::test::OracleMock; - -struct ContextBuilder { - block_number: Option, - contract_address: Option, -} - -impl ContextBuilder { - fn new() -> Self { - Self { block_number: Option::none(), contract_address: Option::none() } - } - - fn block_number(&mut self, block_number: Field) -> &mut Self { - self.block_number = Option::some(block_number); - self - } - - fn contract_address(&mut self, contract_address: AztecAddress) -> &mut Self { - self.contract_address = Option::some(contract_address); - self - } - - fn private(&mut self) -> PrivateContext { - let mut context = PrivateContext::empty(); - - if self.block_number.is_some() { - context.inputs.historical_header.global_variables.block_number = self.block_number.unwrap_unchecked(); - } - - if self.contract_address.is_some() { - context.inputs.call_context.storage_contract_address = self.contract_address.unwrap_unchecked(); - } - - context - } - - fn public(&mut self) -> PublicContext { - let mut context = PublicContext::empty(); - - if self.block_number.is_some() { - let _ = OracleMock::mock("avmOpcodeBlockNumber").returns(self.block_number.unwrap()); - } - - if self.contract_address.is_some() { - let _ = OracleMock::mock("avmOpcodeAddress").returns(self.contract_address.unwrap()); - } - - context - } -} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/keys.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/keys.nr new file mode 100644 index 00000000000..f4c53c95c20 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/keys.nr @@ -0,0 +1,18 @@ +use dep::protocol_types::{ + address::AztecAddress, storage::map::derive_storage_slot_in_map, + constants::CANONICAL_KEY_REGISTRY_ADDRESS, grumpkin_point::GrumpkinPoint +}; + +use crate::test::helpers::cheatcodes; + +pub fn store_master_key(key_index: Field, address: AztecAddress, key: GrumpkinPoint) { + let x_coordinate_map_slot = key_index * 2 + 1; + let y_coordinate_map_slot = x_coordinate_map_slot + 1; + let x_coordinate_derived_slot = derive_storage_slot_in_map(x_coordinate_map_slot, address); + let y_coordinate_derived_slot = derive_storage_slot_in_map(y_coordinate_map_slot, address); + + let canonical_registry_address = AztecAddress::from_field(CANONICAL_KEY_REGISTRY_ADDRESS); + + cheatcodes::direct_storage_write(canonical_registry_address, x_coordinate_derived_slot, [key.x]); + cheatcodes::direct_storage_write(canonical_registry_address, y_coordinate_derived_slot, [key.y]); +} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr new file mode 100644 index 00000000000..142f6fd58f8 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -0,0 +1,207 @@ +use dep::protocol_types::{ + abis::{function_selector::FunctionSelector, private_circuit_public_inputs::PrivateCircuitPublicInputs}, + address::{AztecAddress, PartialAddress}, storage::map::derive_storage_slot_in_map, + constants::CANONICAL_KEY_REGISTRY_ADDRESS, grumpkin_point::GrumpkinPoint, traits::Deserialize +}; + +use crate::context::inputs::{PublicContextInputs, PrivateContextInputs}; +use crate::context::{packed_returns::PackedReturns, call_interfaces::CallInterface}; + +use crate::context::{PrivateContext, PublicContext, PrivateVoidCallInterface}; +use crate::test::helpers::{cheatcodes, types::{Deployer, TestAccount}, keys}; +use crate::keys::constants::{NULLIFIER_INDEX, INCOMING_INDEX, OUTGOING_INDEX, TAGGING_INDEX}; +use crate::hash::hash_args; + +use crate::note::{ + note_header::NoteHeader, note_interface::NoteInterface, + utils::{compute_note_hash_for_insertion, compute_note_hash_for_consumption} +}; +use crate::oracle::notes::notify_created_note; + +struct TestEnvironment { + contract_address: Option, + args_hash: Option, + function_selector: Option +} + +impl TestEnvironment { + fn new() -> Self { + cheatcodes::reset(); + Self { contract_address: Option::none(), args_hash: Option::none(), function_selector: Option::none() } + } + + fn block_number(self) -> u32 { + cheatcodes::get_block_number() + } + + fn advance_block_to(&mut self, block_number: u32) { + let difference = block_number - cheatcodes::get_block_number(); + self.advance_block_by(difference); + } + + fn advance_block_by(&mut self, blocks: u32) { + cheatcodes::advance_blocks(blocks); + } + + fn public(self) -> PublicContext { + PublicContext::empty() + } + + fn private(&mut self) -> PrivateContext { + self.private_at(cheatcodes::get_block_number()) + } + + fn private_at(&mut self, historical_block_number: u32) -> PrivateContext { + if historical_block_number >= cheatcodes::get_block_number() { + self.advance_block_to(historical_block_number + 1); + } + + let mut inputs = cheatcodes::get_private_context_inputs(historical_block_number); + + PrivateContext::new(inputs, 0) + } + + fn create_account(self) -> AztecAddress { + let test_account = cheatcodes::create_account(); + let address = test_account.address; + let keys = test_account.keys; + + keys::store_master_key(NULLIFIER_INDEX, address, keys.npk_m); + keys::store_master_key(INCOMING_INDEX, address, keys.ivpk_m); + keys::store_master_key(OUTGOING_INDEX, address, keys.ovpk_m); + keys::store_master_key(TAGGING_INDEX, address, keys.tpk_m); + + test_account.address + } + + fn create_account_contract(self, secret: Field) -> AztecAddress { + let public_keys = cheatcodes::derive_keys(secret); + let args = &[public_keys.ivpk_m.x, public_keys.ivpk_m.y]; + let address = cheatcodes::deploy( + "@aztec/noir-contracts.js/SchnorrAccount", + "constructor", + args, + public_keys.hash().to_field() + ); + cheatcodes::advance_blocks(1); + let test_account = cheatcodes::add_account(secret, PartialAddress::from_field(address.to_field())); + let address = test_account.address; + let keys = test_account.keys; + + keys::store_master_key(NULLIFIER_INDEX, address, keys.npk_m); + keys::store_master_key(INCOMING_INDEX, address, keys.ivpk_m); + keys::store_master_key(OUTGOING_INDEX, address, keys.ovpk_m); + keys::store_master_key(TAGGING_INDEX, address, keys.tpk_m); + + test_account.address + } + + fn deploy(self, path: str) -> Deployer { + Deployer { path, public_keys_hash: 0 } + } + + fn call_private( + self, + call_interface: C + ) -> T where C: CallInterface, T: Deserialize { + let original_fn = call_interface.get_original(); + let original_msg_sender = cheatcodes::get_msg_sender(); + let original_contract_address = cheatcodes::get_contract_address(); + let target_address = call_interface.get_contract_address(); + + cheatcodes::set_contract_address(target_address); + cheatcodes::set_msg_sender(original_contract_address); + let mut inputs = cheatcodes::get_private_context_inputs(cheatcodes::get_block_number() - 1); + inputs.call_context.function_selector = call_interface.get_selector(); + let public_inputs = original_fn(inputs); + + cheatcodes::set_contract_address(original_contract_address); + cheatcodes::set_msg_sender(original_msg_sender); + PackedReturns::new(public_inputs.returns_hash).unpack_into() + } + + fn call_private_void( + self, + call_interface: C + ) where C: CallInterface { + let original_fn = call_interface.get_original(); + let original_msg_sender = cheatcodes::get_msg_sender(); + let original_contract_address = cheatcodes::get_contract_address(); + let target_address = call_interface.get_contract_address(); + + cheatcodes::set_contract_address(target_address); + cheatcodes::set_msg_sender(original_contract_address); + let mut inputs = cheatcodes::get_private_context_inputs(cheatcodes::get_block_number() - 1); + inputs.call_context.function_selector = call_interface.get_selector(); + let public_inputs = original_fn(inputs); + + cheatcodes::set_contract_address(original_contract_address); + cheatcodes::set_msg_sender(original_msg_sender); + PackedReturns::new(public_inputs.returns_hash).assert_empty(); + } + + fn call_public(self, call_interface: C) -> T where C: CallInterface { + let original_fn = call_interface.get_original(); + let original_msg_sender = cheatcodes::get_msg_sender(); + let original_contract_address = cheatcodes::get_contract_address(); + let target_address = call_interface.get_contract_address(); + + cheatcodes::set_contract_address(target_address); + cheatcodes::set_msg_sender(original_contract_address); + let mut inputs = cheatcodes::get_public_context_inputs(); + inputs.selector = call_interface.get_selector().to_field(); + inputs.args_hash = hash_args(call_interface.get_args()); + let result = original_fn(inputs); + + cheatcodes::set_contract_address(original_contract_address); + cheatcodes::set_msg_sender(original_msg_sender); + result + } + + fn call_public_void(self, call_interface: C) where C: CallInterface { + let original_fn = call_interface.get_original(); + let original_msg_sender = cheatcodes::get_msg_sender(); + let original_contract_address = cheatcodes::get_contract_address(); + let target_address = call_interface.get_contract_address(); + + cheatcodes::set_contract_address(target_address); + cheatcodes::set_msg_sender(original_contract_address); + let mut inputs = cheatcodes::get_public_context_inputs(); + inputs.selector = call_interface.get_selector().to_field(); + inputs.args_hash = hash_args(call_interface.get_args()); + original_fn(inputs); + + cheatcodes::set_contract_address(original_contract_address); + cheatcodes::set_msg_sender(original_msg_sender); + } + + pub fn store_note_in_cache( + self, + note: &mut Note, + storage_slot: Field, + contract_address: AztecAddress + ) where Note: NoteInterface { + let original_contract_address = cheatcodes::get_contract_address(); + cheatcodes::set_contract_address(contract_address); + let note_hash_counter = cheatcodes::get_side_effects_counter(); + + let header = NoteHeader { contract_address, storage_slot, nonce: 0, note_hash_counter }; + // TODO: change this to note.set_header(header) once https://github.com/noir-lang/noir/issues/4095 is fixed + Note::set_header(note, header); + let inner_note_hash = compute_note_hash_for_insertion(*note); + + // TODO: Strong typing required because of https://github.com/noir-lang/noir/issues/4088 + let serialized_note: [Field; N] = Note::serialize_content(*note); + assert( + notify_created_note( + storage_slot, + Note::get_note_type_id(), + serialized_note, + inner_note_hash, + note_hash_counter + ) + == 0 + ); + cheatcodes::set_contract_address(original_contract_address); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/types.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/types.nr new file mode 100644 index 00000000000..7baec3523d8 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/types.nr @@ -0,0 +1,110 @@ +use dep::protocol_types::{ + traits::{Deserialize, Serialize}, address::AztecAddress, + abis::{function_selector::FunctionSelector, private_circuit_public_inputs::PrivateCircuitPublicInputs} +}; + +use crate::context::inputs::{PublicContextInputs, PrivateContextInputs}; +use crate::context::call_interfaces::CallInterface; +use crate::test::helpers::cheatcodes; +use crate::keys::public_keys::{PUBLIC_KEYS_LENGTH, PublicKeys}; +use crate::hash::hash_args; + +struct Deployer { + path: str, + public_keys_hash: Field + } + +impl Deployer { + pub fn with_private_initializer( + self, + call_interface: C + ) -> AztecAddress where C: CallInterface { + let address = cheatcodes::deploy( + self.path, + call_interface.get_name(), + call_interface.get_args(), + self.public_keys_hash + ); + cheatcodes::advance_blocks(1); + let block_number = cheatcodes::get_block_number(); + let original_fn = call_interface.get_original(); + let original_msg_sender = cheatcodes::get_msg_sender(); + let original_contract_address = cheatcodes::get_contract_address(); + + cheatcodes::set_contract_address(address); + cheatcodes::set_msg_sender(original_contract_address); + let mut inputs = cheatcodes::get_private_context_inputs(block_number - 1); + inputs.call_context.function_selector = call_interface.get_selector(); + let _result = original_fn(inputs); + + cheatcodes::set_contract_address(original_contract_address); + cheatcodes::set_msg_sender(original_msg_sender); + address + } + + pub fn with_public_initializer( + self, + call_interface: C + ) -> AztecAddress where C: CallInterface { + let address = cheatcodes::deploy( + self.path, + call_interface.get_name(), + call_interface.get_args(), + self.public_keys_hash + ); + cheatcodes::advance_blocks(1); + let original_fn = call_interface.get_original(); + let original_msg_sender = cheatcodes::get_msg_sender(); + let original_contract_address = cheatcodes::get_contract_address(); + + cheatcodes::set_contract_address(address); + cheatcodes::set_msg_sender(original_contract_address); + let mut inputs = cheatcodes::get_public_context_inputs(); + inputs.selector = call_interface.get_selector().to_field(); + inputs.args_hash = hash_args(call_interface.get_args()); + let _result: T = original_fn(inputs); + + cheatcodes::set_contract_address(original_contract_address); + cheatcodes::set_msg_sender(original_msg_sender); + address + } + + pub fn without_initializer(self) -> AztecAddress { + let address = cheatcodes::deploy(self.path, "", &[], self.public_keys_hash); + address + } +} + +// Keys length + address +global TEST_ACCOUNT_LENGTH = PUBLIC_KEYS_LENGTH + 1; + +struct TestAccount { + address: AztecAddress, + keys: PublicKeys +} + +impl Serialize for TestAccount { + fn serialize(self) -> [Field; TEST_ACCOUNT_LENGTH] { + let mut output = [0; TEST_ACCOUNT_LENGTH]; + + output[0] = self.address.to_field(); + + for i in 0..PUBLIC_KEYS_LENGTH { + output[i+1] = self.keys.serialize()[i]; + } + output + } +} + +impl Deserialize for TestAccount { + fn deserialize(input: [Field; TEST_ACCOUNT_LENGTH]) -> Self { + let address = AztecAddress::from_field(input[0]); + let mut key_buffer = [0; PUBLIC_KEYS_LENGTH]; + for i in 0..PUBLIC_KEYS_LENGTH { + key_buffer[i] = input[i+1]; + } + let keys = PublicKeys::deserialize(key_buffer); + + Self { address, keys } + } +} diff --git a/noir-projects/aztec-nr/aztec/src/test/mocks.nr b/noir-projects/aztec-nr/aztec/src/test/mocks.nr index fb8ef27c7bf..148cc284a9f 100644 --- a/noir-projects/aztec-nr/aztec/src/test/mocks.nr +++ b/noir-projects/aztec-nr/aztec/src/test/mocks.nr @@ -1 +1,2 @@ mod mock_note; +mod mock_struct; diff --git a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr new file mode 100644 index 00000000000..1a0ed1a304e --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_struct.nr @@ -0,0 +1,36 @@ +use dep::protocol_types::traits::{Eq, Serialize, Deserialize}; + +struct MockStruct { + a: Field, + b: Field, +} + +impl MockStruct { + fn new(a: Field, b: Field) -> Self { + Self { a, b } + } +} + +impl Eq for MockStruct { + fn eq(self, other: Self) -> bool { + (self.a == other.a) & (self.b == other.b) + } +} + +impl Serialize<2> for MockStruct { + fn serialize(self) -> [Field; 2] { + [self.a, self.b] + } +} + +impl Deserialize<2> for MockStruct { + fn deserialize(fields: [Field; 2]) -> Self { + Self { a: fields[0], b: fields[1] } + } +} + +#[test] +fn test_serde() { + let val = MockStruct::new(5, 6); + assert_eq(val, MockStruct::deserialize(val.serialize())); +} 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 43bfe011063..6a34f458cce 100644 --- a/noir-projects/noir-contracts/contracts/child_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/child_contract/src/main.nr @@ -56,7 +56,7 @@ contract Child { let owner_npk_m_hash = header.get_npk_m_hash(&mut context, owner); let mut note = ValueNote::new(new_value, owner_npk_m_hash); - storage.a_map_with_private_values.at(owner).insert(&mut note).emit(encode_and_encrypt(&mut context, context.msg_sender(), owner)); + storage.a_map_with_private_values.at(owner).insert(&mut note).emit(encode_and_encrypt(&mut context, owner, owner)); new_value } 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 cf2a235e798..e296d211aab 100644 --- a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr @@ -37,4 +37,29 @@ contract Counter { balance_utils::get_balance(counters.at(owner).set) } // docs:end:get_counter + + use dep::aztec::test::{helpers::{cheatcodes, test_environment::TestEnvironment}}; + use dep::aztec::protocol_types::storage::map::derive_storage_slot_in_map; + use dep::aztec::note::note_getter::{MAX_NOTES_PER_PAGE, view_notes}; + use dep::aztec::note::note_viewer_options::NoteViewerOptions; + + #[test] + fn test_initialize() { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + let owner = env.create_account(); + let outgoing_viewer = env.create_account(); + + // Deploy contract and initialize + let initializer = Counter::interface().initialize(5, owner, outgoing_viewer); + let contract_address = env.deploy("@aztec/noir-contracts.js/Counter").with_private_initializer(initializer); + // Read the stored value in the note + + cheatcodes::set_contract_address(contract_address); + let counter_slot = Counter::storage().counters.slot; + let owner_slot = derive_storage_slot_in_map(counter_slot, owner); + let mut options = NoteViewerOptions::new(); + let opt_notes: [Option; MAX_NOTES_PER_PAGE] = view_notes(owner_slot, options); + assert(opt_notes[0].unwrap().value == 5); + } } diff --git a/noir-projects/noir-contracts/contracts/parent_contract/Nargo.toml b/noir-projects/noir-contracts/contracts/parent_contract/Nargo.toml index 53a4d614564..a91d3bd758c 100644 --- a/noir-projects/noir-contracts/contracts/parent_contract/Nargo.toml +++ b/noir-projects/noir-contracts/contracts/parent_contract/Nargo.toml @@ -6,3 +6,5 @@ type = "contract" [dependencies] aztec = { path = "../../../aztec-nr/aztec" } +child_contract = { path = "../child_contract" } +value_note = { path = "../../../aztec-nr/value-note" } 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 fb066de5ad6..e6901b1a319 100644 --- a/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr @@ -242,4 +242,49 @@ contract Parent { // Call the target private function context.static_call_public_function(target_contract, target_selector, args); } + + use dep::aztec::test::{helpers::{cheatcodes, test_environment::TestEnvironment}}; + use dep::aztec::protocol_types::storage::map::derive_storage_slot_in_map; + use dep::aztec::note::note_getter::{MAX_NOTES_PER_PAGE, view_notes}; + use dep::aztec::note::note_viewer_options::NoteViewerOptions; + use dep::child_contract::Child; + use dep::value_note::value_note::ValueNote; + + #[test] + fn test_private_call() { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + let owner = env.create_account(); + + // Deploy child contract + let child_contract_address = env.deploy("@aztec/noir-contracts.js/Child").without_initializer(); + cheatcodes::advance_blocks(1); + + // Set value in child through parent + let value_to_set = 7; + let parent_private_set_call_interface = Parent::interface().private_call( + child_contract_address, + FunctionSelector::from_signature("private_set_value(Field,(Field))"), + [value_to_set, owner.to_field()] + ); + let result: Field = env.call_private(parent_private_set_call_interface); + assert(result == value_to_set); + // Read the stored value in the note. We have to change the contract address to the child contract in order to read its notes + cheatcodes::set_contract_address(child_contract_address); + let counter_slot = Child::storage().a_map_with_private_values.slot; + let owner_slot = derive_storage_slot_in_map(counter_slot, owner); + let mut options = NoteViewerOptions::new(); + let opt_notes: [Option; MAX_NOTES_PER_PAGE] = view_notes(owner_slot, options); + let note_value = opt_notes[0].unwrap().value; + assert(note_value == value_to_set); + assert(note_value == result); + // Get value from child through parent + let parent_private_get_call_interface = Parent::interface().private_call( + child_contract_address, + FunctionSelector::from_signature("private_get_value(Field,(Field))"), + [7, owner.to_field()] + ); + let read_result: Field = env.call_private(parent_private_get_call_interface); + assert(note_value == read_result); + } } diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index c68563ec1b1..79046b74aa0 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -486,12 +486,12 @@ contract Test { // This function is used in the e2e_state_vars to test the SharedMutablePrivateGetter in isolation #[aztec(private)] - fn test_shared_mutable_private_getter( + fn test_shared_mutable_private_getter( contract_address_to_read: AztecAddress, storage_slot_of_shared_mutable: Field - ) -> Field where T: FromField, T: ToField { + ) -> Field { // It's a bit wonky because we need to know the delay for get_current_value_in_private to work correctly - let test: SharedMutablePrivateGetter = SharedMutablePrivateGetter::new( + let test: SharedMutablePrivateGetter = SharedMutablePrivateGetter::new( &mut context, contract_address_to_read, storage_slot_of_shared_mutable diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 4c6db0c5d4d..fca6ed10b8f 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -395,5 +395,75 @@ contract Token { storage.balances.balance_of(owner).to_field() } // docs:end:balance_of_private + + use dep::aztec::test::{helpers::{cheatcodes, test_environment::TestEnvironment}}; + use dep::aztec::protocol_types::storage::map::derive_storage_slot_in_map; + use dep::aztec::note::note_getter::{MAX_NOTES_PER_PAGE, view_notes}; + use dep::aztec::note::note_viewer_options::NoteViewerOptions; + + #[test] + fn test_private_transfer() { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + let owner = env.create_account(); + let recipient = env.create_account(); + let mint_amount = 10000; + + // Start the test in the account contract address + cheatcodes::set_contract_address(owner); + + // Deploy token contract + let initializer_call_interface = Token::interface().constructor( + owner, + "TestToken0000000000000000000000", + "TT00000000000000000000000000000", + 18 + ); + let token_contract_address = env.deploy("@aztec/noir-contracts.js/Token").with_public_initializer(initializer_call_interface); + env.advance_block_by(1); + + // Mint some tokens + let secret = 1; + let secret_hash = compute_secret_hash(secret); + let mint_private_call_interface = Token::at(token_contract_address).mint_private(mint_amount, secret_hash); + env.call_public(mint_private_call_interface); + + // Time travel so we can read keys from the registry + env.advance_block_by(6); + + // Store a note in the cache so we can redeem it + env.store_note_in_cache( + &mut TransparentNote::new(mint_amount, secret_hash), + Token::storage().pending_shields.slot, + token_contract_address + ); + + // Redeem our shielded tokens + let redeem_shield_call_interface = Token::at(token_contract_address).redeem_shield(owner, mint_amount, secret); + env.call_private_void(redeem_shield_call_interface); + + // Not really sure why this is needed? Nullifier inclusion in contract initializer fails otherwise. + // If it were to fail, it should do it at line 443, investigation required + env.advance_block_by(1); + + // Transfer tokens + let transfer_amount = 1000; + let private_token_transfer_call_interface = Token::at(token_contract_address).transfer(recipient, transfer_amount); + env.call_private_void(private_token_transfer_call_interface); + + // Check balances + cheatcodes::set_contract_address(token_contract_address); + + let balances_slot = Token::storage().balances.slot; + let recipient_slot = derive_storage_slot_in_map(balances_slot, recipient); + let mut options = NoteViewerOptions::new(); + let opt_notes: [Option; MAX_NOTES_PER_PAGE] = view_notes(recipient_slot, options); + assert(opt_notes[0].unwrap().amount.to_field() == transfer_amount); + + let owner_slot = derive_storage_slot_in_map(balances_slot, owner); + let mut options = NoteViewerOptions::new(); + let opt_notes: [Option; MAX_NOTES_PER_PAGE] = view_notes(owner_slot, options); + assert(opt_notes[0].unwrap().amount.to_field() == mint_amount - transfer_amount); + } } // docs:end:token_all \ No newline at end of file diff --git a/noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr b/noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr index d00374bb4e4..3bf3f00fb1b 100644 --- a/noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/uniswap_contract/src/main.nr @@ -21,14 +21,14 @@ contract Uniswap { struct Storage { portal_address: SharedImmutable, } - + #[aztec(public)] #[aztec(initializer)] fn constructor(portal_address: EthAddress) { storage.portal_address.initialize(portal_address); } // docs:end:uniswap_setup - + // docs:start:swap_public #[aztec(public)] fn swap_public( diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/traits.nr b/noir-projects/noir-protocol-circuits/crates/types/src/traits.nr index bce2078479a..01af1206fec 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/traits.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/traits.nr @@ -110,4 +110,10 @@ impl Serialize for str { trait Deserialize { fn deserialize(fields: [Field; N]) -> Self; } -// docs:end:deserialize \ No newline at end of file +// docs:end:deserialize + +impl Deserialize for [Field; N] { + fn deserialize(fields: [Field; N]) -> Self { + fields + } +} diff --git a/noir/noir-repo/aztec_macros/src/lib.rs b/noir/noir-repo/aztec_macros/src/lib.rs index a36b7b17d09..d79c7b190ed 100644 --- a/noir/noir-repo/aztec_macros/src/lib.rs +++ b/noir/noir-repo/aztec_macros/src/lib.rs @@ -65,14 +65,8 @@ fn transform( // Usage -> mut ast -> aztec_library::transform(&mut ast) // Covers all functions in the ast for submodule in ast.submodules.iter_mut().filter(|submodule| submodule.is_contract) { - if transform_module( - crate_id, - &file_id, - context, - &mut submodule.contents, - submodule.name.0.contents.as_str(), - ) - .map_err(|err| (err.into(), file_id))? + if transform_module(&file_id, &mut submodule.contents, submodule.name.0.contents.as_str()) + .map_err(|err| (err.into(), file_id))? { check_for_aztec_dependency(crate_id, context)?; } @@ -87,9 +81,7 @@ fn transform( /// For annotated functions it calls the `transform` function which will perform the required transformations. /// Returns true if an annotated node is found, false otherwise fn transform_module( - crate_id: &CrateId, file_id: &FileId, - context: &HirContext, module: &mut SortedModule, module_name: &str, ) -> Result { @@ -106,12 +98,7 @@ fn transform_module( if !check_for_storage_implementation(module, storage_struct_name) { generate_storage_implementation(module, storage_struct_name)?; } - // Make sure we're only generating the storage layout for the root crate - // In case we got a contract importing other contracts for their interface, we - // don't want to generate the storage layout for them - if crate_id == context.root_crate_id() { - generate_storage_layout(module, storage_struct_name.clone())?; - } + generate_storage_layout(module, storage_struct_name.clone(), module_name)?; } for structure in module.types.iter_mut() { @@ -219,7 +206,7 @@ fn transform_module( }); } - generate_contract_interface(module, module_name, &stubs)?; + generate_contract_interface(module, module_name, &stubs, storage_defined)?; } Ok(has_transformed_module) diff --git a/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs b/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs index 90f9ce6164a..1875ab0b252 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs @@ -61,11 +61,7 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call let parameters = func.parameters(); let is_void = if matches!(fn_return_type.typ, UnresolvedTypeData::Unit) { "Void" } else { "" }; let is_static = if is_static_call { "Static" } else { "" }; - let return_type_hint = if is_void == "Void" { - "".to_string() - } else { - format!("<{}>", fn_return_type.typ.to_string().replace("plain::", "")) - }; + let return_type_hint = fn_return_type.typ.to_string().replace("plain::", ""); let call_args = parameters .iter() .map(|arg| { @@ -73,22 +69,67 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call match &arg.typ.typ { UnresolvedTypeData::Array(_, typ) => { format!( - "let hash_{0} = {0}.map(|x: {1}| x.serialize()); - for i in 0..{0}.len() {{ - args_acc = args_acc.append(hash_{0}[i].as_slice()); - }}\n", + "let serialized_{0} = {0}.map(|x: {1}| x.serialize()); + for i in 0..{0}.len() {{ + args_acc = args_acc.append(serialized_{0}[i].as_slice()); + }}\n", param_name, typ.typ.to_string().replace("plain::", "") ) } - _ => { + UnresolvedTypeData::Named(_, _, _) | UnresolvedTypeData::String(_) => { format!("args_acc = args_acc.append({}.serialize().as_slice());\n", param_name) } + _ => { + format!("args_acc = args_acc.append(&[{}.to_field()]);\n", param_name) + } } }) .collect::>() .join(""); - if aztec_visibility != "Public" { + + let param_types = if !parameters.is_empty() { + parameters + .iter() + .map(|param| param.pattern.name_ident().0.contents.clone()) + .collect::>() + .join(", ") + } else { + "".to_string() + }; + + let original = format!( + "| inputs: dep::aztec::context::inputs::{}ContextInputs | -> {} {{ + {}(inputs{}) + }}", + aztec_visibility, + if aztec_visibility == "Private" { + "dep::aztec::protocol_types::abis::private_circuit_public_inputs::PrivateCircuitPublicInputs".to_string() + } else { + return_type_hint.clone() + }, + fn_name, + if param_types.is_empty() { "".to_string() } else { format!(" ,{} ", param_types) } + ); + let arg_types = format!( + "({}{})", + parameters + .iter() + .map(|param| param.typ.typ.to_string().replace("plain::", "")) + .collect::>() + .join(","), + // In order to distinguish between a single element Tuple (Type,) and a single type with unnecessary parenthesis around it (Type), + // The latter gets simplified to Type, that is NOT a valid env + if parameters.len() == 1 { "," } else { "" } + ); + + let generics = if is_void == "Void" { + format!("{}>", arg_types) + } else { + format!("{}, {}>", return_type_hint, arg_types) + }; + + let fn_body = if aztec_visibility != "Public" { let args_hash = if !parameters.is_empty() { format!( "let mut args_acc: [Field] = &[]; @@ -98,23 +139,25 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call call_args ) } else { - "let args_hash = 0;".to_string() + " + let mut args_acc: [Field] = &[]; + let args_hash = 0; + " + .to_string() }; - let fn_body = format!( - "{} - dep::aztec::context::{}{}{}CallInterface {{ - target_contract: self.target_contract, - selector: {}, - args_hash, - }}", - args_hash, aztec_visibility, is_static, is_void, fn_selector, - ); format!( - "pub fn {}(self, {}) -> dep::aztec::context::{}{}{}CallInterface{} {{ - {} - }}", - fn_name, fn_parameters, aztec_visibility, is_static, is_void, return_type_hint, fn_body + "{} + let selector = {}; + dep::aztec::context::{}{}{}CallInterface {{ + target_contract: self.target_contract, + selector, + name: \"{}\", + args_hash, + args: args_acc, + original: {} + }}", + args_hash, fn_selector, aztec_visibility, is_static, is_void, fn_name, original ) } else { let args = format!( @@ -123,23 +166,34 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call ", call_args ); - let fn_body = format!( + format!( "{} - dep::aztec::context::Public{}{}CallInterface {{ + let selector = {}; + dep::aztec::context::{}{}{}CallInterface {{ target_contract: self.target_contract, - selector: {}, + selector, + name: \"{}\", args: args_acc, gas_opts: dep::aztec::context::gas::GasOpts::default(), + original: {} }}", - args, is_static, is_void, fn_selector, - ); - format!( - "pub fn {}(self, {}) -> dep::aztec::context::Public{}{}CallInterface{} {{ - {} - }}", - fn_name, fn_parameters, is_static, is_void, return_type_hint, fn_body + args, fn_selector, aztec_visibility, is_static, is_void, fn_name, original ) - } + }; + + format!( + "pub fn {}(self, {}) -> dep::aztec::context::{}{}{}CallInterface<{},{} {{ + {} + }}", + fn_name, + fn_parameters, + aztec_visibility, + is_static, + is_void, + fn_name.len(), + generics, + fn_body + ) } // Generates the contract interface as a struct with an `at` function that holds the stubbed functions and provides @@ -149,7 +203,15 @@ pub fn generate_contract_interface( module: &mut SortedModule, module_name: &str, stubs: &[(String, Location)], + has_storage_layout: bool, ) -> Result<(), AztecMacroError> { + let storage_layout_getter = format!( + "#[contract_library_method] + pub fn storage() -> StorageLayout {{ + {}_STORAGE_LAYOUT + }}", + module_name, + ); let contract_interface = format!( " struct {0} {{ @@ -164,6 +226,12 @@ pub fn generate_contract_interface( ) -> Self {{ Self {{ target_contract }} }} + + pub fn interface() -> Self {{ + Self {{ target_contract: dep::aztec::protocol_types::address::AztecAddress::zero() }} + }} + + {2} }} #[contract_library_method] @@ -172,9 +240,18 @@ pub fn generate_contract_interface( ) -> {0} {{ {0} {{ target_contract }} }} + + #[contract_library_method] + pub fn interface() -> {0} {{ + {0} {{ target_contract: dep::aztec::protocol_types::address::AztecAddress::zero() }} + }} + + {3} ", module_name, stubs.iter().map(|(src, _)| src.to_owned()).collect::>().join("\n"), + if has_storage_layout { storage_layout_getter.clone() } else { "".to_string() }, + if has_storage_layout { format!("#[contract_library_method]\n{}", storage_layout_getter) } else { "".to_string() } ); let (contract_interface_ast, errors) = parse_program(&contract_interface); @@ -191,7 +268,7 @@ pub fn generate_contract_interface( .iter() .enumerate() .map(|(i, (method, orig_span))| { - if method.name() == "at" { + if method.name() == "at" || method.name() == "interface" || method.name() == "storage" { (method.clone(), *orig_span) } else { let (_, new_location) = stubs[i]; @@ -205,7 +282,9 @@ pub fn generate_contract_interface( module.types.push(contract_interface_ast.types.pop().unwrap()); module.impls.push(impl_with_locations); - module.functions.push(contract_interface_ast.functions.pop().unwrap()); + for function in contract_interface_ast.functions { + module.functions.push(function); + } Ok(()) } @@ -244,7 +323,7 @@ pub fn update_fn_signatures_in_contract_interface( let name = context.def_interner.function_name(func_id); let fn_parameters = &context.def_interner.function_meta(func_id).parameters.clone(); - if name == "at" { + if name == "at" || name == "interface" || name == "storage" { continue; } @@ -257,42 +336,29 @@ pub fn update_fn_signatures_in_contract_interface( .collect::>(), ); let hir_func = context.def_interner.function(func_id).block(&context.def_interner); - let call_interface_constructor_statement = context.def_interner.statement( - hir_func - .statements() - .last() - .ok_or((AztecMacroError::AztecDepNotFound, file_id))?, + + let function_selector_statement = context.def_interner.statement( + hir_func.statements().get(hir_func.statements().len() - 2).ok_or(( + AztecMacroError::CouldNotGenerateContractInterface { + secondary_message: Some( + "Function signature statement not found, invalid body length" + .to_string(), + ), + }, + file_id, + ))?, ); - let call_interface_constructor_expression = - match call_interface_constructor_statement { - HirStatement::Expression(expression_id) => { - match context.def_interner.expression(&expression_id) { - HirExpression::Constructor(hir_constructor_expression) => { - Ok(hir_constructor_expression) - } - _ => Err(( - AztecMacroError::CouldNotGenerateContractInterface { - secondary_message: Some( - "CallInterface constructor statement must be a constructor expression" - .to_string(), - ), - }, - file_id, - )), - } - } - _ => Err(( - AztecMacroError::CouldNotGenerateContractInterface { - secondary_message: Some( - "CallInterface constructor statement must be an expression" - .to_string(), - ), - }, - file_id, - )), - }?; - let (_, function_selector_expression_id) = - call_interface_constructor_expression.fields[1]; + let function_selector_expression_id = match function_selector_statement { + HirStatement::Let(let_statement) => Ok(let_statement.expression), + _ => Err(( + AztecMacroError::CouldNotGenerateContractInterface { + secondary_message: Some( + "Function selector statement must be an expression".to_string(), + ), + }, + file_id, + )), + }?; let function_selector_expression = context.def_interner.expression(&function_selector_expression_id); diff --git a/noir/noir-repo/aztec_macros/src/transforms/storage.rs b/noir/noir-repo/aztec_macros/src/transforms/storage.rs index a1c21c7efcf..bac87502c7d 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/storage.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/storage.rs @@ -497,6 +497,7 @@ pub fn assign_storage_slots( pub fn generate_storage_layout( module: &mut SortedModule, storage_struct_name: String, + module_name: &str, ) -> Result<(), AztecMacroError> { let definition = module .types @@ -504,33 +505,28 @@ pub fn generate_storage_layout( .find(|r#struct| r#struct.name.0.contents == *storage_struct_name) .unwrap(); - let mut generic_args = vec![]; let mut storable_fields = vec![]; let mut storable_fields_impl = vec![]; - definition.fields.iter().enumerate().for_each(|(index, (field_ident, field_type))| { - storable_fields.push(format!("{}: dep::aztec::prelude::Storable", field_ident, index)); - generic_args.push(format!("N{}", index)); - storable_fields_impl.push(format!( - "{}: dep::aztec::prelude::Storable {{ slot: 0, typ: \"{}\" }}", - field_ident, - field_type.to_string().replace("plain::", "") - )); + definition.fields.iter().for_each(|(field_ident, _)| { + storable_fields.push(format!("{}: dep::aztec::prelude::Storable", field_ident)); + storable_fields_impl + .push(format!("{}: dep::aztec::prelude::Storable {{ slot: 0 }}", field_ident,)); }); let storage_fields_source = format!( " - struct StorageLayout<{}> {{ + struct StorageLayout {{ {} }} #[abi(storage)] - global STORAGE_LAYOUT = StorageLayout {{ + global {}_STORAGE_LAYOUT = StorageLayout {{ {} }}; ", - generic_args.join(", "), storable_fields.join(",\n"), + module_name, storable_fields_impl.join(",\n") ); diff --git a/noir/noir-repo/compiler/noirc_frontend/src/monomorphization/mod.rs b/noir/noir-repo/compiler/noirc_frontend/src/monomorphization/mod.rs index 7ecea5c9eac..6dfd575d2b2 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/monomorphization/mod.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/monomorphization/mod.rs @@ -241,7 +241,9 @@ impl<'interner> Monomorphizer<'interner> { Definition::Oracle(opcode.to_string()) } FunctionKind::Recursive => { - unreachable!("Only main can be specified as recursive, which should already be checked"); + let id = + self.queue_function(id, expr_id, typ, turbofish_generics, trait_method); + Definition::Function(id) } } } diff --git a/noir/noir-repo/docs/docs/how_to/how-to-oracles.md b/noir/noir-repo/docs/docs/how_to/how-to-oracles.md index 62ead1f534f..64d984c8cf9 100644 --- a/noir/noir-repo/docs/docs/how_to/how-to-oracles.md +++ b/noir/noir-repo/docs/docs/how_to/how-to-oracles.md @@ -141,10 +141,10 @@ server.addMethod("resolve_function_call", async (params) => { if params.function !== "getSqrt" { throw Error("Unexpected foreign call") }; - const values = params.inputs[0].Array.map((field) => { + const values = params.inputs[0].map((field) => { return `${Math.sqrt(parseInt(field, 16))}`; }); - return { values: [{ Array: values }] }; + return { values }; }); ``` @@ -232,9 +232,9 @@ const foreignCallHandler = async (name, input) => { // notice that the "inputs" parameter contains *all* the inputs // in this case we to make the RPC request with the first parameter "numbers", which would be input[0] const oracleReturn = await client.request(name, [ - { Array: input[0].map((i) => i.toString("hex")) }, + input[0].map((i) => i.toString("hex")), ]); - return [oracleReturn.values[0].Array]; + return { values: oracleReturn }; }; // the rest of your NoirJS code diff --git a/yarn-project/Earthfile b/yarn-project/Earthfile index 14b724a28b5..f39379e20a1 100644 --- a/yarn-project/Earthfile +++ b/yarn-project/Earthfile @@ -115,6 +115,19 @@ rollup-verifier-contract: RUN --entrypoint write-contract -c RootRollupArtifact -n UltraVerifier.sol SAVE ARTIFACT /usr/src/bb /usr/src/bb +txe: + FROM +build + RUN yarn workspaces focus @aztec/txe --production && yarn cache clean + # Remove a bunch of stuff that we don't need that takes up space. + RUN rm -rf \ + ../noir-projects \ + ../l1-contracts \ + ../barretenberg/ts/src \ + ../barretenberg/ts/dest/node-cjs \ + ../barretenberg/ts/dest/browser \ + **/artifacts + SAVE ARTIFACT /usr/src /usr/src + aztec-prod: FROM +build RUN yarn workspaces focus @aztec/aztec @aztec/builder --production && yarn cache clean diff --git a/yarn-project/archiver/src/archiver/index.ts b/yarn-project/archiver/src/archiver/index.ts index a7294537624..81aa8727e17 100644 --- a/yarn-project/archiver/src/archiver/index.ts +++ b/yarn-project/archiver/src/archiver/index.ts @@ -3,3 +3,4 @@ export * from './config.js'; export { MemoryArchiverStore } from './memory_archiver_store/memory_archiver_store.js'; export { ArchiverDataStore } from './archiver_store.js'; export { KVArchiverDataStore } from './kv_archiver_store/kv_archiver_store.js'; +export { ContractInstanceStore } from './kv_archiver_store/contract_instance_store.js'; diff --git a/yarn-project/builder/src/contract-interface-gen/typescript.ts b/yarn-project/builder/src/contract-interface-gen/typescript.ts index 614cc21e631..2a51d433bc9 100644 --- a/yarn-project/builder/src/contract-interface-gen/typescript.ts +++ b/yarn-project/builder/src/contract-interface-gen/typescript.ts @@ -194,10 +194,9 @@ function generateStorageLayoutGetter(input: ContractArtifact) { const storageFieldsUnionType = entries.map(([name]) => `'${name}'`).join(' | '); const layout = entries .map( - ([name, { slot, typ }]) => + ([name, { slot }]) => `${name}: { slot: new Fr(${slot.toBigInt()}n), - typ: "${typ}", }`, ) .join(',\n'); diff --git a/yarn-project/circuits.js/src/contract/contract_instance.ts b/yarn-project/circuits.js/src/contract/contract_instance.ts index 1d45791cdab..3168be0ce0c 100644 --- a/yarn-project/circuits.js/src/contract/contract_instance.ts +++ b/yarn-project/circuits.js/src/contract/contract_instance.ts @@ -1,11 +1,20 @@ -import { type ContractArtifact, type FunctionArtifact, getDefaultInitializer } from '@aztec/foundation/abi'; +import { + type ContractArtifact, + type FunctionArtifact, + FunctionSelector, + getDefaultInitializer, +} from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; import { type ContractInstance, type ContractInstanceWithAddress } from '@aztec/types/contracts'; import { getContractClassFromArtifact } from '../contract/contract_class.js'; import { computeContractClassId } from '../contract/contract_class_id.js'; -import { computeContractAddressFromInstance, computeInitializationHash } from './contract_address.js'; +import { + computeContractAddressFromInstance, + computeInitializationHash, + computeInitializationHashFromEncodedArgs, +} from './contract_address.js'; /** * Generates a Contract Instance from the deployment params. @@ -18,6 +27,7 @@ export function getContractInstanceFromDeployParams( opts: { constructorArtifact?: FunctionArtifact | string; constructorArgs?: any[]; + skipArgsDecoding?: boolean; salt?: Fr; publicKeysHash?: Fr; deployer?: AztecAddress; @@ -27,10 +37,15 @@ export function getContractInstanceFromDeployParams( const salt = opts.salt ?? Fr.random(); const constructorArtifact = getConstructorArtifact(artifact, opts.constructorArtifact); const deployer = opts.deployer ?? AztecAddress.ZERO; - const contractClass = getContractClassFromArtifact(artifact); const contractClassId = computeContractClassId(contractClass); - const initializationHash = computeInitializationHash(constructorArtifact, args); + const initializationHash = + constructorArtifact && opts?.skipArgsDecoding + ? computeInitializationHashFromEncodedArgs( + FunctionSelector.fromNameAndParameters(constructorArtifact?.name, constructorArtifact?.parameters), + args, + ) + : computeInitializationHash(constructorArtifact, args); const publicKeysHash = opts.publicKeysHash ?? Fr.ZERO; const instance: ContractInstance = { diff --git a/yarn-project/deploy_npm.sh b/yarn-project/deploy_npm.sh index f8d18abdac7..b7154d1226c 100755 --- a/yarn-project/deploy_npm.sh +++ b/yarn-project/deploy_npm.sh @@ -105,3 +105,4 @@ deploy_package p2p deploy_package prover-client deploy_package sequencer-client deploy_package aztec-node +deploy_package txe diff --git a/yarn-project/foundation/src/abi/abi.ts b/yarn-project/foundation/src/abi/abi.ts index 200e7b0088c..2b608086604 100644 --- a/yarn-project/foundation/src/abi/abi.ts +++ b/yarn-project/foundation/src/abi/abi.ts @@ -290,10 +290,6 @@ export type FieldLayout = { * Slot in which the field is stored. */ slot: Fr; - /** - * Type being stored at the slot (e.g., 'Map>') - */ - typ: string; }; /** diff --git a/yarn-project/package.json b/yarn-project/package.json index fa08f05b063..388c8f4d6df 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -51,6 +51,7 @@ "sequencer-client", "scripts", "types", + "txe", "world-state" ], "prettier": "@aztec/foundation/prettier", diff --git a/yarn-project/pxe/src/database/index.ts b/yarn-project/pxe/src/database/index.ts index 4685cc2f7a8..e01c1803241 100644 --- a/yarn-project/pxe/src/database/index.ts +++ b/yarn-project/pxe/src/database/index.ts @@ -1 +1,2 @@ export * from './pxe_database.js'; +export * from './kv_pxe_database.js'; diff --git a/yarn-project/pxe/src/index.ts b/yarn-project/pxe/src/index.ts index d7cf6d57253..7c62b24d3ff 100644 --- a/yarn-project/pxe/src/index.ts +++ b/yarn-project/pxe/src/index.ts @@ -9,3 +9,5 @@ export * from '@aztec/foundation/fields'; export * from '@aztec/foundation/eth-address'; export * from '@aztec/foundation/aztec-address'; export * from '@aztec/key-store'; +export * from './database/index.js'; +export { ContractDataOracle } from './contract_data_oracle/index.js'; diff --git a/yarn-project/simulator/src/client/execution_note_cache.ts b/yarn-project/simulator/src/client/execution_note_cache.ts index 325e0b8f80b..3615b012c99 100644 --- a/yarn-project/simulator/src/client/execution_note_cache.ts +++ b/yarn-project/simulator/src/client/execution_note_cache.ts @@ -63,7 +63,6 @@ export class ExecutionNoteCache { nullifiedNoteHashCounter = note.counter; this.newNotes.set(contractAddress.toBigInt(), notes); } - return nullifiedNoteHashCounter; } diff --git a/yarn-project/simulator/src/client/index.ts b/yarn-project/simulator/src/client/index.ts index 60fcf15d4a9..bb30eff1312 100644 --- a/yarn-project/simulator/src/client/index.ts +++ b/yarn-project/simulator/src/client/index.ts @@ -1,3 +1,5 @@ export * from './simulator.js'; export * from './db_oracle.js'; export * from './execution_result.js'; +export * from './pick_notes.js'; +export * from './execution_note_cache.js'; diff --git a/yarn-project/txe/.eslintrc.cjs b/yarn-project/txe/.eslintrc.cjs new file mode 100644 index 00000000000..e659927475c --- /dev/null +++ b/yarn-project/txe/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = require('@aztec/foundation/eslint'); diff --git a/yarn-project/txe/package.json b/yarn-project/txe/package.json new file mode 100644 index 00000000000..5658cd3b454 --- /dev/null +++ b/yarn-project/txe/package.json @@ -0,0 +1,82 @@ +{ + "name": "@aztec/txe", + "version": "0.0.0", + "type": "module", + "exports": "./dest/index.js", + "bin": "./dest/bin/index.js", + "typedocOptions": { + "entryPoints": [ + "./src/index.ts" + ], + "name": "TXE", + "tsconfig": "./tsconfig.json" + }, + "scripts": { + "build": "yarn clean && tsc -b", + "build:dev": "tsc -b --watch", + "clean": "rm -rf ./dest .tsbuildinfo", + "formatting": "run -T prettier --check ./src && run -T eslint ./src", + "formatting:fix": "run -T eslint --fix ./src && run -T prettier -w ./src", + "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests", + "start": "DEBUG='aztec:*' && node ./dest/bin/index.js" + }, + "inherits": [ + "../package.common.json" + ], + "jest": { + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.[cm]?js$": "$1" + }, + "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$", + "rootDir": "./src", + "workerThreads": true, + "transform": { + "^.+\\.tsx?$": [ + "@swc/jest" + ] + }, + "extensionsToTreatAsEsm": [ + ".ts" + ], + "reporters": [ + [ + "default", + { + "summaryThreshold": 9999 + } + ] + ] + }, + "dependencies": { + "@aztec/archiver": "workspace:^", + "@aztec/aztec.js": "workspace:^", + "@aztec/circuit-types": "workspace:^", + "@aztec/circuits.js": "workspace:^", + "@aztec/foundation": "workspace:^", + "@aztec/key-store": "workspace:^", + "@aztec/kv-store": "workspace:^", + "@aztec/noir-contracts.js": "workspace:^", + "@aztec/pxe": "workspace:^", + "@aztec/simulator": "workspace:^", + "@aztec/types": "workspace:^", + "@aztec/world-state": "workspace:^" + }, + "devDependencies": { + "@jest/globals": "^29.5.0", + "@types/jest": "^29.5.0", + "@types/node": "^18.7.23", + "jest": "^29.5.0", + "jest-mock-extended": "^3.0.3", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "files": [ + "dest", + "src", + "!*.test.*" + ], + "types": "./dest/index.d.ts", + "engines": { + "node": ">=18" + } +} diff --git a/yarn-project/txe/src/bin/index.ts b/yarn-project/txe/src/bin/index.ts new file mode 100644 index 00000000000..14934762159 --- /dev/null +++ b/yarn-project/txe/src/bin/index.ts @@ -0,0 +1,86 @@ +#!/usr/bin/env -S node --no-warnings +import { JsonRpcServer } from '@aztec/foundation/json-rpc/server'; +import { type Logger, createDebugLogger } from '@aztec/foundation/log'; + +import http from 'http'; + +import { TXEService } from '../txe_service/txe_service.js'; +import { type ForeignCallResult, toForeignCallResult } from '../util/encoding.js'; + +const { TXE_PORT = 8080 } = process.env; + +const logger = createDebugLogger('aztec:txe_service'); + +const TXESessions = new Map(); + +type MethodNames = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; +}[keyof T]; + +type TXEForeignCallInput = { + session_id: number; + function: MethodNames | 'reset'; + inputs: any[]; +}; + +class TXEDispatcher { + constructor(private logger: Logger) {} + + // eslint-disable-next-line camelcase + async resolve_foreign_call({ + session_id: sessionId, + function: functionName, + inputs, + }: TXEForeignCallInput): Promise { + this.logger.debug( + `Calling ${functionName} with inputs: ${JSON.stringify(inputs, null, 2)} on session ${sessionId}`, + ); + + if (!TXESessions.has(sessionId) && functionName != 'reset') { + this.logger.debug(`Creating new session ${sessionId}`); + TXESessions.set(sessionId, await TXEService.init(logger)); + } + + if (functionName === 'reset') { + TXESessions.delete(sessionId) && + this.logger.debug(`Called reset on session ${sessionId}, yeeting it out of existence`); + return toForeignCallResult([]); + } else { + const txeService = TXESessions.get(sessionId); + const response = await (txeService as any)[functionName](...inputs); + this.logger.debug( + `${sessionId}:${functionName}(${JSON.stringify(inputs, null, 2)}) -> ${JSON.stringify(response, null, 2)}`, + ); + return response; + } + } +} + +/** + * Creates an http server that forwards calls to the TXE and starts it on the given port. + * @param txeService - TXE that answers queries to the created HTTP server. + * @param port - Port to listen in. + * @returns A running http server. + */ +export function startTXEHttpServer(dispatcher: TXEDispatcher, port: string | number): http.Server { + const txeServer = new JsonRpcServer(dispatcher, {}, {}, ['init']); + + const app = txeServer.getApp(); + const httpServer = http.createServer(app.callback()); + httpServer.listen(port); + + return httpServer; +} + +/** + * Create and start a new TXE HTTP Server + */ +function main() { + logger.info(`Setting up TXE...`); + + startTXEHttpServer(new TXEDispatcher(logger), TXE_PORT); + + logger.info(`TXE listening on port ${TXE_PORT}`); +} + +main(); diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts new file mode 100644 index 00000000000..09115f3ebab --- /dev/null +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -0,0 +1,596 @@ +import { + L1NotePayload, + MerkleTreeId, + Note, + type NoteStatus, + NullifierMembershipWitness, + PublicDataWitness, + PublicDataWrite, + TaggedLog, + type UnencryptedL2Log, +} from '@aztec/circuit-types'; +import { type CircuitWitnessGenerationStats } from '@aztec/circuit-types/stats'; +import { + type CompleteAddress, + FunctionData, + type Header, + type KeyValidationRequest, + NULLIFIER_SUBTREE_HEIGHT, + type NULLIFIER_TREE_HEIGHT, + type NullifierLeafPreimage, + PUBLIC_DATA_SUBTREE_HEIGHT, + type PUBLIC_DATA_TREE_HEIGHT, + PrivateCallStackItem, + PrivateCircuitPublicInputs, + PrivateContextInputs, + type PublicCallRequest, + PublicDataTreeLeaf, + type PublicDataTreeLeafPreimage, + computeContractClassId, + deriveKeys, + getContractClassFromArtifact, +} from '@aztec/circuits.js'; +import { Aes128 } from '@aztec/circuits.js/barretenberg'; +import { computePublicDataTreeLeafSlot, siloNoteHash, siloNullifier } from '@aztec/circuits.js/hash'; +import { type ContractArtifact, type FunctionAbi, FunctionSelector, countArgumentsSize } from '@aztec/foundation/abi'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { Fr, GrumpkinScalar, type Point } from '@aztec/foundation/fields'; +import { type Logger, applyStringFormatting } from '@aztec/foundation/log'; +import { Timer } from '@aztec/foundation/timer'; +import { type KeyStore } from '@aztec/key-store'; +import { ContractDataOracle } from '@aztec/pxe'; +import { + ExecutionError, + type ExecutionNoteCache, + type MessageLoadOracleInputs, + type NoteData, + Oracle, + type PackedValuesCache, + type TypedOracle, + acvm, + createSimulationError, + extractCallStack, + pickNotes, + toACVMWitness, + witnessMapToFields, +} from '@aztec/simulator'; +import { type ContractInstance, type ContractInstanceWithAddress } from '@aztec/types/contracts'; +import { MerkleTreeSnapshotOperationsFacade, type MerkleTrees } from '@aztec/world-state'; + +import { type TXEDatabase } from '../util/txe_database.js'; + +export class TXE implements TypedOracle { + private blockNumber = 0; + private sideEffectsCounter = 0; + private contractAddress: AztecAddress; + private msgSender: AztecAddress; + + private contractDataOracle: ContractDataOracle; + + constructor( + private logger: Logger, + private trees: MerkleTrees, + private packedValuesCache: PackedValuesCache, + private noteCache: ExecutionNoteCache, + private keyStore: KeyStore, + private txeDatabase: TXEDatabase, + ) { + this.contractDataOracle = new ContractDataOracle(txeDatabase); + this.contractAddress = AztecAddress.random(); + this.msgSender = AztecAddress.fromField(new Fr(0)); + } + + // Utils + + getMsgSender() { + return this.msgSender; + } + + setMsgSender(msgSender: Fr) { + this.msgSender = msgSender; + } + + getSideEffectsCounter() { + return this.sideEffectsCounter; + } + + setSideEffectsCounter(sideEffectsCounter: number) { + this.sideEffectsCounter = sideEffectsCounter; + } + + setContractAddress(contractAddress: AztecAddress) { + this.contractAddress = contractAddress; + } + + setBlockNumber(blockNumber: number) { + this.blockNumber = blockNumber; + } + + getTrees() { + return this.trees; + } + + getTXEDatabase() { + return this.txeDatabase; + } + + getKeyStore() { + return this.keyStore; + } + + async addContractInstance(contractInstance: ContractInstanceWithAddress) { + await this.txeDatabase.addContractInstance(contractInstance); + } + + async addContractArtifact(artifact: ContractArtifact) { + const contractClass = getContractClassFromArtifact(artifact); + await this.txeDatabase.addContractArtifact(computeContractClassId(contractClass), artifact); + } + + async getPrivateContextInputs(blockNumber: number, sideEffectsCounter = this.sideEffectsCounter) { + const trees = this.getTrees(); + const stateReference = await trees.getStateReference(true); + const inputs = PrivateContextInputs.empty(); + inputs.historicalHeader.globalVariables.blockNumber = new Fr(blockNumber); + inputs.historicalHeader.state = stateReference; + inputs.callContext.msgSender = this.msgSender; + inputs.callContext.storageContractAddress = this.contractAddress; + inputs.callContext.sideEffectCounter = sideEffectsCounter; + inputs.startSideEffectCounter = sideEffectsCounter; + return inputs; + } + + getPublicContextInputs() { + const inputs = { + functionSelector: FunctionSelector.fromField(new Fr(0)), + argsHash: new Fr(0), + isStaticCall: false, + toFields: function () { + return [this.functionSelector.toField(), this.argsHash, new Fr(this.isStaticCall)]; + }, + }; + return inputs; + } + + async avmOpcodeNullifierExists(innerNullifier: Fr, targetAddress: AztecAddress): Promise { + const nullifier = siloNullifier(targetAddress, innerNullifier!); + const db = this.trees.asLatest(); + const index = await db.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()); + return index !== undefined; + } + + async avmOpcodeEmitNullifier(nullifier: Fr) { + const db = this.trees.asLatest(); + const siloedNullifier = siloNullifier(this.contractAddress, nullifier); + await db.batchInsert(MerkleTreeId.NULLIFIER_TREE, [siloedNullifier.toBuffer()], NULLIFIER_SUBTREE_HEIGHT); + return Promise.resolve(); + } + + async avmOpcodeEmitNoteHash(innerNoteHash: Fr) { + const db = this.trees.asLatest(); + const noteHash = siloNoteHash(this.contractAddress, innerNoteHash); + await db.appendLeaves(MerkleTreeId.NOTE_HASH_TREE, [noteHash]); + return Promise.resolve(); + } + + deriveKeys(secret: Fr) { + return deriveKeys(secret); + } + + // TypedOracle + + getBlockNumber(): Promise { + return Promise.resolve(this.blockNumber); + } + + getContractAddress(): Promise { + return Promise.resolve(this.contractAddress); + } + + getRandomField() { + return Fr.random(); + } + + packArgumentsArray(args: Fr[]): Promise { + return Promise.resolve(this.packedValuesCache.pack(args)); + } + + packReturns(returns: Fr[]): Promise { + return Promise.resolve(this.packedValuesCache.pack(returns)); + } + + unpackReturns(returnsHash: Fr): Promise { + return Promise.resolve(this.packedValuesCache.unpack(returnsHash)); + } + + getKeyValidationRequest(pkMHash: Fr): Promise { + return this.keyStore.getKeyValidationRequest(pkMHash, this.contractAddress); + } + + async getContractInstance(address: AztecAddress): Promise { + const contractInstance = await this.txeDatabase.getContractInstance(address); + if (!contractInstance) { + throw new Error(`Contract instance not found for address ${address}`); + } + return Promise.resolve(contractInstance); + } + + getMembershipWitness(_blockNumber: number, _treeId: MerkleTreeId, _leafValue: Fr): Promise { + throw new Error('Method not implemented.'); + } + + async getSiblingPath(blockNumber: number, treeId: MerkleTreeId, leafIndex: Fr) { + const committedDb = new MerkleTreeSnapshotOperationsFacade(this.trees, blockNumber); + const result = await committedDb.getSiblingPath(treeId, leafIndex.toBigInt()); + return result.toFields(); + } + + async getNullifierMembershipWitness( + blockNumber: number, + nullifier: Fr, + ): Promise { + const committedDb = new MerkleTreeSnapshotOperationsFacade(this.trees, blockNumber); + const index = await committedDb.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()); + if (!index) { + return undefined; + } + + const leafPreimagePromise = committedDb.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index); + const siblingPathPromise = committedDb.getSiblingPath( + MerkleTreeId.NULLIFIER_TREE, + BigInt(index), + ); + + const [leafPreimage, siblingPath] = await Promise.all([leafPreimagePromise, siblingPathPromise]); + + if (!leafPreimage) { + return undefined; + } + + return new NullifierMembershipWitness(BigInt(index), leafPreimage as NullifierLeafPreimage, siblingPath); + } + + async getPublicDataTreeWitness(blockNumber: number, leafSlot: Fr): Promise { + const committedDb = new MerkleTreeSnapshotOperationsFacade(this.trees, blockNumber); + const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt()); + if (!lowLeafResult) { + return undefined; + } else { + const preimage = (await committedDb.getLeafPreimage( + MerkleTreeId.PUBLIC_DATA_TREE, + lowLeafResult.index, + )) as PublicDataTreeLeafPreimage; + const path = await committedDb.getSiblingPath( + MerkleTreeId.PUBLIC_DATA_TREE, + lowLeafResult.index, + ); + return new PublicDataWitness(lowLeafResult.index, preimage, path); + } + } + + getLowNullifierMembershipWitness( + _blockNumber: number, + _nullifier: Fr, + ): Promise { + throw new Error('Method not implemented.'); + } + + getHeader(_blockNumber: number): Promise
{ + throw new Error('Method not implemented.'); + } + + getCompleteAddress(account: AztecAddress): Promise { + return Promise.resolve(this.txeDatabase.getAccount(account)); + } + + getAuthWitness(_messageHash: Fr): Promise { + throw new Error('Method not implemented.'); + } + + popCapsule(): Promise { + throw new Error('Method not implemented.'); + } + + getNotes( + storageSlot: Fr, + numSelects: number, + selectByIndexes: number[], + selectByOffsets: number[], + selectByLengths: number[], + selectValues: Fr[], + selectComparators: number[], + sortByIndexes: number[], + sortByOffsets: number[], + sortByLengths: number[], + sortOrder: number[], + limit: number, + offset: number, + _status: NoteStatus, + ) { + // Nullified pending notes are already removed from the list. + const pendingNotes = this.noteCache.getNotes(this.contractAddress, storageSlot); + + const notes = pickNotes(pendingNotes, { + selects: selectByIndexes.slice(0, numSelects).map((index, i) => ({ + selector: { index, offset: selectByOffsets[i], length: selectByLengths[i] }, + value: selectValues[i], + comparator: selectComparators[i], + })), + sorts: sortByIndexes.map((index, i) => ({ + selector: { index, offset: sortByOffsets[i], length: sortByLengths[i] }, + order: sortOrder[i], + })), + limit, + offset, + }); + + this.logger.debug( + `Returning ${notes.length} notes for ${this.contractAddress} at ${storageSlot}: ${notes + .map(n => `${n.nonce.toString()}:[${n.note.items.map(i => i.toString()).join(',')}]`) + .join(', ')}`, + ); + + return Promise.resolve(notes); + } + + async notifyCreatedNote(storageSlot: Fr, noteTypeId: Fr, noteItems: Fr[], innerNoteHash: Fr, counter: number) { + const note = new Note(noteItems); + this.noteCache.addNewNote( + { + contractAddress: this.contractAddress, + storageSlot, + nonce: Fr.ZERO, // Nonce cannot be known during private execution. + note, + siloedNullifier: undefined, // Siloed nullifier cannot be known for newly created note. + innerNoteHash, + }, + counter, + ); + const db = this.trees.asLatest(); + const noteHash = siloNoteHash(this.contractAddress, innerNoteHash); + await db.appendLeaves(MerkleTreeId.NOTE_HASH_TREE, [noteHash]); + } + + async notifyNullifiedNote(innerNullifier: Fr, innerNoteHash: Fr, _counter: number) { + this.noteCache.nullifyNote(this.contractAddress, innerNullifier, innerNoteHash); + const db = this.trees.asLatest(); + const siloedNullifier = siloNullifier(this.contractAddress, innerNullifier); + await db.batchInsert(MerkleTreeId.NULLIFIER_TREE, [siloedNullifier.toBuffer()], NULLIFIER_SUBTREE_HEIGHT); + return Promise.resolve(); + } + + async checkNullifierExists(innerNullifier: Fr): Promise { + const nullifier = siloNullifier(this.contractAddress, innerNullifier!); + const db = this.trees.asLatest(); + const index = await db.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()); + return index !== undefined; + } + + getL1ToL2MembershipWitness( + _contractAddress: AztecAddress, + _messageHash: Fr, + _secret: Fr, + ): Promise> { + throw new Error('Method not implemented.'); + } + + async storageRead(startStorageSlot: Fr, numberOfElements: number): Promise { + const db = this.trees.asLatest(); + + const values = []; + for (let i = 0n; i < numberOfElements; i++) { + const storageSlot = startStorageSlot.add(new Fr(i)); + const leafSlot = computePublicDataTreeLeafSlot(this.contractAddress, storageSlot).toBigInt(); + + const lowLeafResult = await db.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot); + + let value = Fr.ZERO; + if (lowLeafResult && lowLeafResult.alreadyPresent) { + const preimage = (await db.getLeafPreimage( + MerkleTreeId.PUBLIC_DATA_TREE, + lowLeafResult.index, + )) as PublicDataTreeLeafPreimage; + value = preimage.value; + } + this.logger.debug(`Oracle storage read: slot=${storageSlot.toString()} value=${value}`); + values.push(value); + } + return values; + } + + async storageWrite(startStorageSlot: Fr, values: Fr[]): Promise { + const db = this.trees.asLatest(); + + const publicDataWrites = values.map((value, i) => { + const storageSlot = startStorageSlot.add(new Fr(i)); + this.logger.debug(`Oracle storage write: slot=${storageSlot.toString()} value=${value}`); + return new PublicDataWrite(computePublicDataTreeLeafSlot(this.contractAddress, storageSlot), value); + }); + await db.batchInsert( + MerkleTreeId.PUBLIC_DATA_TREE, + publicDataWrites.map(write => new PublicDataTreeLeaf(write.leafIndex, write.newValue).toBuffer()), + PUBLIC_DATA_SUBTREE_HEIGHT, + ); + return publicDataWrites.map(write => write.newValue); + } + + emitEncryptedLog(_contractAddress: AztecAddress, _randomness: Fr, _encryptedNote: Buffer, _counter: number): void { + return; + } + + emitEncryptedNoteLog(_noteHashCounter: number, _encryptedNote: Buffer, _counter: number): void { + return; + } + + computeEncryptedNoteLog( + contractAddress: AztecAddress, + storageSlot: Fr, + noteTypeId: Fr, + ovKeys: KeyValidationRequest, + ivpkM: Point, + preimage: Fr[], + ): Buffer { + const note = new Note(preimage); + const l1NotePayload = new L1NotePayload(note, contractAddress, storageSlot, noteTypeId); + const taggedNote = new TaggedLog(l1NotePayload); + + const ephSk = GrumpkinScalar.random(); + + const recipient = AztecAddress.random(); + + return taggedNote.encrypt(ephSk, recipient, ivpkM, ovKeys); + } + + emitUnencryptedLog(_log: UnencryptedL2Log, _counter: number): void { + throw new Error('Method not implemented.'); + } + + emitContractClassUnencryptedLog(_log: UnencryptedL2Log, _counter: number): Fr { + throw new Error('Method not implemented.'); + } + + async callPrivateFunction( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + argsHash: Fr, + sideEffectCounter: number, + _isStaticCall: boolean, + _isDelegateCall: boolean, + ): Promise { + this.logger.debug( + `Calling private function ${targetContractAddress}:${functionSelector} from ${this.contractAddress}`, + ); + // Store and modify env + const currentContractAddress = AztecAddress.fromField(this.contractAddress); + const currentMessageSender = AztecAddress.fromField(this.msgSender); + this.setMsgSender(this.contractAddress); + this.setContractAddress(targetContractAddress); + + const artifact = await this.contractDataOracle.getFunctionArtifact(targetContractAddress, functionSelector); + + const acir = artifact.bytecode; + const initialWitness = await this.getInitialWitness(artifact, argsHash, sideEffectCounter); + const acvmCallback = new Oracle(this); + const timer = new Timer(); + const acirExecutionResult = await acvm(acir, initialWitness, acvmCallback).catch((err: Error) => { + const execError = new ExecutionError( + err.message, + { + contractAddress: targetContractAddress, + functionSelector, + }, + extractCallStack(err, artifact.debug), + { cause: err }, + ); + this.logger.debug( + `Error executing private function ${targetContractAddress}:${functionSelector}\n${createSimulationError( + execError, + )}`, + ); + throw execError; + }); + const duration = timer.ms(); + const returnWitness = witnessMapToFields(acirExecutionResult.returnWitness); + const publicInputs = PrivateCircuitPublicInputs.fromFields(returnWitness); + + const initialWitnessSize = witnessMapToFields(initialWitness).length * Fr.SIZE_IN_BYTES; + this.logger.debug(`Ran external function ${targetContractAddress.toString()}:${functionSelector}`, { + circuitName: 'app-circuit', + duration, + eventName: 'circuit-witness-generation', + inputSize: initialWitnessSize, + outputSize: publicInputs.toBuffer().length, + appCircuitName: 'noname', + } satisfies CircuitWitnessGenerationStats); + + const callStackItem = new PrivateCallStackItem( + targetContractAddress, + new FunctionData(functionSelector, true), + publicInputs, + ); + // Apply side effects + this.sideEffectsCounter += publicInputs.endSideEffectCounter.toNumber(); + this.setContractAddress(currentContractAddress); + this.setMsgSender(currentMessageSender); + + return callStackItem; + } + + async getInitialWitness(abi: FunctionAbi, argsHash: Fr, sideEffectCounter: number) { + const argumentsSize = countArgumentsSize(abi); + + const args = this.packedValuesCache.unpack(argsHash); + + if (args.length !== argumentsSize) { + throw new Error('Invalid arguments size'); + } + + const privateContextInputs = await this.getPrivateContextInputs(this.blockNumber - 1, sideEffectCounter); + + const fields = [...privateContextInputs.toFields(), ...args]; + + return toACVMWitness(0, fields); + } + + callPublicFunction( + _targetContractAddress: AztecAddress, + _functionSelector: FunctionSelector, + _argsHash: Fr, + _sideEffectCounter: number, + _isStaticCall: boolean, + _isDelegateCall: boolean, + ): Promise { + throw new Error('Method not implemented.'); + } + + enqueuePublicFunctionCall( + _targetContractAddress: AztecAddress, + _functionSelector: FunctionSelector, + _argsHash: Fr, + _sideEffectCounter: number, + _isStaticCall: boolean, + _isDelegateCall: boolean, + ): Promise { + throw new Error('Method not implemented.'); + } + + setPublicTeardownFunctionCall( + _targetContractAddress: AztecAddress, + _functionSelector: FunctionSelector, + _argsHash: Fr, + _sideEffectCounter: number, + _isStaticCall: boolean, + _isDelegateCall: boolean, + ): Promise { + throw new Error('Method not implemented.'); + } + + aes128Encrypt(input: Buffer, initializationVector: Buffer, key: Buffer): Buffer { + const aes128 = new Aes128(); + return aes128.encryptBufferCBC(input, initializationVector, key); + } + + debugLog(message: string, fields: Fr[]): void { + this.logger.verbose(`debug_log ${applyStringFormatting(message, fields)}`); + } + + emitEncryptedEventLog( + _contractAddress: AztecAddress, + _randomness: Fr, + _encryptedEvent: Buffer, + _counter: number, + ): void { + return; + } + + computeEncryptedEventLog( + _contractAddress: AztecAddress, + _randomness: Fr, + _eventTypeId: Fr, + _ovKeys: KeyValidationRequest, + _ivpkM: Point, + _preimage: Fr[], + ): Buffer { + throw new Error('Method not implemented.'); + } +} diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts new file mode 100644 index 00000000000..6b53c0de1eb --- /dev/null +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -0,0 +1,522 @@ +import { L2Block, MerkleTreeId, PublicDataWrite } from '@aztec/circuit-types'; +import { + Fr, + FunctionSelector, + Header, + KeyValidationRequest, + PUBLIC_DATA_SUBTREE_HEIGHT, + Point, + PublicDataTreeLeaf, + getContractInstanceFromDeployParams, +} from '@aztec/circuits.js'; +import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; +import { AztecAddress } from '@aztec/foundation/aztec-address'; +import { type Logger } from '@aztec/foundation/log'; +import { KeyStore } from '@aztec/key-store'; +import { type AztecKVStore } from '@aztec/kv-store'; +import { openTmpStore } from '@aztec/kv-store/utils'; +import { ExecutionNoteCache, PackedValuesCache, type TypedOracle } from '@aztec/simulator'; +import { MerkleTrees } from '@aztec/world-state'; + +import { TXE } from '../oracle/txe_oracle.js'; +import { + type ForeignCallArray, + type ForeignCallSingle, + fromArray, + fromSingle, + toArray, + toForeignCallResult, + toSingle, +} from '../util/encoding.js'; +import { TXEDatabase } from '../util/txe_database.js'; + +export class TXEService { + constructor(private logger: Logger, private typedOracle: TypedOracle, private store: AztecKVStore) {} + + static async init(logger: Logger) { + const store = openTmpStore(true); + const trees = await MerkleTrees.new(store, logger); + const packedValuesCache = new PackedValuesCache(); + const noteCache = new ExecutionNoteCache(); + const keyStore = new KeyStore(store); + const txeDatabase = new TXEDatabase(store); + logger.info(`TXE service initialized`); + const txe = new TXE(logger, trees, packedValuesCache, noteCache, keyStore, txeDatabase); + const service = new TXEService(logger, txe, store); + await service.timeTravel(toSingle(new Fr(1n))); + return service; + } + + // Cheatcodes + + async getPrivateContextInputs(blockNumber: ForeignCallSingle) { + const inputs = await (this.typedOracle as TXE).getPrivateContextInputs(fromSingle(blockNumber).toNumber()); + return toForeignCallResult(inputs.toFields().map(toSingle)); + } + + getPublicContextInputs() { + const inputs = (this.typedOracle as TXE).getPublicContextInputs(); + return toForeignCallResult(inputs.toFields().map(toSingle)); + } + + async timeTravel(blocks: ForeignCallSingle) { + const nBlocks = fromSingle(blocks).toNumber(); + this.logger.info(`time traveling ${nBlocks} blocks`); + const trees = (this.typedOracle as TXE).getTrees(); + for (let i = 0; i < nBlocks; i++) { + const header = Header.empty(); + const l2Block = L2Block.empty(); + header.state = await trees.getStateReference(true); + const blockNumber = await this.typedOracle.getBlockNumber(); + + header.globalVariables.blockNumber = new Fr(blockNumber); + header.state.partial.nullifierTree.root = Fr.fromBuffer( + (await trees.getTreeInfo(MerkleTreeId.NULLIFIER_TREE, true)).root, + ); + header.state.partial.noteHashTree.root = Fr.fromBuffer( + (await trees.getTreeInfo(MerkleTreeId.NOTE_HASH_TREE, true)).root, + ); + header.state.partial.publicDataTree.root = Fr.fromBuffer( + (await trees.getTreeInfo(MerkleTreeId.PUBLIC_DATA_TREE, true)).root, + ); + header.state.l1ToL2MessageTree.root = Fr.fromBuffer( + (await trees.getTreeInfo(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, true)).root, + ); + l2Block.archive.root = Fr.fromBuffer((await trees.getTreeInfo(MerkleTreeId.ARCHIVE, true)).root); + l2Block.header = header; + await trees.handleL2BlockAndMessages(l2Block, []); + (this.typedOracle as TXE).setBlockNumber(blockNumber + 1); + } + return toForeignCallResult([]); + } + + setContractAddress(address: ForeignCallSingle) { + const typedAddress = AztecAddress.fromField(fromSingle(address)); + (this.typedOracle as TXE).setContractAddress(typedAddress); + return toForeignCallResult([]); + } + + deriveKeys(secret: ForeignCallSingle) { + const keys = (this.typedOracle as TXE).deriveKeys(fromSingle(secret)); + return toForeignCallResult(keys.publicKeys.toFields().map(toSingle)); + } + + async deploy( + path: ForeignCallArray, + initializer: ForeignCallArray, + _length: ForeignCallSingle, + args: ForeignCallArray, + publicKeysHash: ForeignCallSingle, + ) { + const pathStr = fromArray(path) + .map(char => String.fromCharCode(char.toNumber())) + .join(''); + const initializerStr = fromArray(initializer) + .map(char => String.fromCharCode(char.toNumber())) + .join(''); + const decodedArgs = fromArray(args); + this.logger.debug(`Deploy ${pathStr} with ${initializerStr} and ${decodedArgs}`); + const contractModule = await import(pathStr); + // Hacky way of getting the class, the name of the Artifact is always longer + const contractClass = contractModule[Object.keys(contractModule).sort((a, b) => a.length - b.length)[0]]; + const instance = getContractInstanceFromDeployParams(contractClass.artifact, { + constructorArgs: decodedArgs, + skipArgsDecoding: true, + salt: Fr.ONE, + publicKeysHash: fromSingle(publicKeysHash), + constructorArtifact: initializerStr ? initializerStr : undefined, + deployer: AztecAddress.ZERO, + }); + + this.logger.debug(`Deployed ${contractClass.artifact.name} at ${instance.address}`); + await (this.typedOracle as TXE).addContractInstance(instance); + await (this.typedOracle as TXE).addContractArtifact(contractClass.artifact); + return toForeignCallResult([toSingle(instance.address)]); + } + + async directStorageWrite( + contractAddress: ForeignCallSingle, + startStorageSlot: ForeignCallSingle, + values: ForeignCallArray, + ) { + const trees = (this.typedOracle as TXE).getTrees(); + const startStorageSlotFr = fromSingle(startStorageSlot); + const valuesFr = fromArray(values); + const contractAddressFr = fromSingle(contractAddress); + const db = trees.asLatest(); + + const publicDataWrites = valuesFr.map((value, i) => { + const storageSlot = startStorageSlotFr.add(new Fr(i)); + this.logger.debug(`Oracle storage write: slot=${storageSlot.toString()} value=${value}`); + return new PublicDataWrite(computePublicDataTreeLeafSlot(contractAddressFr, storageSlot), value); + }); + await db.batchInsert( + MerkleTreeId.PUBLIC_DATA_TREE, + publicDataWrites.map(write => new PublicDataTreeLeaf(write.leafIndex, write.newValue).toBuffer()), + PUBLIC_DATA_SUBTREE_HEIGHT, + ); + return toForeignCallResult([toArray(publicDataWrites.map(write => write.newValue))]); + } + + async createAccount() { + const keyStore = (this.typedOracle as TXE).getKeyStore(); + const completeAddress = await keyStore.createAccount(); + const accountStore = (this.typedOracle as TXE).getTXEDatabase(); + await accountStore.setAccount(completeAddress.address, completeAddress); + this.logger.debug(`Created account ${completeAddress.address}`); + return toForeignCallResult([ + toSingle(completeAddress.address), + ...completeAddress.publicKeys.toFields().map(toSingle), + ]); + } + + async addAccount(secret: ForeignCallSingle, partialAddress: ForeignCallSingle) { + const keyStore = (this.typedOracle as TXE).getKeyStore(); + const completeAddress = await keyStore.addAccount(fromSingle(secret), fromSingle(partialAddress)); + const accountStore = (this.typedOracle as TXE).getTXEDatabase(); + await accountStore.setAccount(completeAddress.address, completeAddress); + return toForeignCallResult([ + toSingle(completeAddress.address), + ...completeAddress.publicKeys.toFields().map(toSingle), + ]); + } + + setMsgSender(msgSender: ForeignCallSingle) { + (this.typedOracle as TXE).setMsgSender(fromSingle(msgSender)); + return toForeignCallResult([]); + } + + getMsgSender() { + const msgSender = (this.typedOracle as TXE).getMsgSender(); + return toForeignCallResult([toSingle(msgSender)]); + } + + getSideEffectsCounter() { + const counter = (this.typedOracle as TXE).getSideEffectsCounter(); + return toForeignCallResult([toSingle(new Fr(counter))]); + } + + // PXE oracles + + getRandomField() { + return toForeignCallResult([toSingle(this.typedOracle.getRandomField())]); + } + + async getContractAddress() { + const contractAddress = await this.typedOracle.getContractAddress(); + return toForeignCallResult([toSingle(contractAddress.toField())]); + } + + async getBlockNumber() { + const blockNumber = await this.typedOracle.getBlockNumber(); + return toForeignCallResult([toSingle(new Fr(blockNumber))]); + } + + async avmOpcodeAddress() { + const contractAddress = await this.typedOracle.getContractAddress(); + return toForeignCallResult([toSingle(contractAddress.toField())]); + } + + async avmOpcodeBlockNumber() { + const blockNumber = await this.typedOracle.getBlockNumber(); + return toForeignCallResult([toSingle(new Fr(blockNumber))]); + } + + async packArgumentsArray(args: ForeignCallArray) { + const packed = await this.typedOracle.packArgumentsArray(fromArray(args)); + return toForeignCallResult([toSingle(packed)]); + } + + async packArguments(_length: ForeignCallSingle, values: ForeignCallArray) { + const packed = await this.typedOracle.packArgumentsArray(fromArray(values)); + return toForeignCallResult([toSingle(packed)]); + } + + // Since the argument is a slice, noir automatically adds a length field to oracle call. + async packReturns(_length: ForeignCallSingle, values: ForeignCallArray) { + const packed = await this.typedOracle.packReturns(fromArray(values)); + return toForeignCallResult([toSingle(packed)]); + } + + async unpackReturns(returnsHash: ForeignCallSingle) { + const unpacked = await this.typedOracle.unpackReturns(fromSingle(returnsHash)); + return toForeignCallResult([toArray(unpacked)]); + } + + // Since the argument is a slice, noir automatically adds a length field to oracle call. + debugLog(message: ForeignCallArray, _length: ForeignCallSingle, fields: ForeignCallArray) { + const messageStr = fromArray(message) + .map(field => String.fromCharCode(field.toNumber())) + .join(''); + const fieldsFr = fromArray(fields); + this.typedOracle.debugLog(messageStr, fieldsFr); + return toForeignCallResult([]); + } + + async storageRead(startStorageSlot: ForeignCallSingle, numberOfElements: ForeignCallSingle) { + const values = await this.typedOracle.storageRead( + fromSingle(startStorageSlot), + fromSingle(numberOfElements).toNumber(), + ); + return toForeignCallResult([toArray(values)]); + } + + async storageWrite(startStorageSlot: ForeignCallSingle, values: ForeignCallArray) { + const newValues = await this.typedOracle.storageWrite(fromSingle(startStorageSlot), fromArray(values)); + return toForeignCallResult([toArray(newValues)]); + } + + async getPublicDataTreeWitness(blockNumber: ForeignCallSingle, leafSlot: ForeignCallSingle) { + const parsedBlockNumber = fromSingle(blockNumber).toNumber(); + const parsedLeafSlot = fromSingle(leafSlot); + + const witness = await this.typedOracle.getPublicDataTreeWitness(parsedBlockNumber, parsedLeafSlot); + if (!witness) { + throw new Error(`Public data witness not found for slot ${parsedLeafSlot} at block ${parsedBlockNumber}.`); + } + return toForeignCallResult([toArray(witness.toFields())]); + } + + async getSiblingPath(blockNumber: ForeignCallSingle, treeId: ForeignCallSingle, leafIndex: ForeignCallSingle) { + const result = await this.typedOracle.getSiblingPath( + fromSingle(blockNumber).toNumber(), + fromSingle(treeId).toNumber(), + fromSingle(leafIndex), + ); + return toForeignCallResult([toArray(result)]); + } + + async getNotes( + storageSlot: ForeignCallSingle, + numSelects: ForeignCallSingle, + selectByIndexes: ForeignCallArray, + selectByOffsets: ForeignCallArray, + selectByLengths: ForeignCallArray, + selectValues: ForeignCallArray, + selectComparators: ForeignCallArray, + sortByIndexes: ForeignCallArray, + sortByOffsets: ForeignCallArray, + sortByLengths: ForeignCallArray, + sortOrder: ForeignCallArray, + limit: ForeignCallSingle, + offset: ForeignCallSingle, + status: ForeignCallSingle, + returnSize: ForeignCallSingle, + ) { + const noteDatas = await this.typedOracle.getNotes( + fromSingle(storageSlot), + fromSingle(numSelects).toNumber(), + fromArray(selectByIndexes).map(fr => fr.toNumber()), + fromArray(selectByOffsets).map(fr => fr.toNumber()), + fromArray(selectByLengths).map(fr => fr.toNumber()), + fromArray(selectValues), + fromArray(selectComparators).map(fr => fr.toNumber()), + fromArray(sortByIndexes).map(fr => fr.toNumber()), + fromArray(sortByOffsets).map(fr => fr.toNumber()), + fromArray(sortByLengths).map(fr => fr.toNumber()), + fromArray(sortOrder).map(fr => fr.toNumber()), + fromSingle(limit).toNumber(), + fromSingle(offset).toNumber(), + fromSingle(status).toNumber(), + ); + const noteLength = noteDatas?.[0]?.note.items.length ?? 0; + if (!noteDatas.every(({ note }) => noteLength === note.items.length)) { + throw new Error('Notes should all be the same length.'); + } + + const contractAddress = noteDatas[0]?.contractAddress ?? Fr.ZERO; + + // Values indicates whether the note is settled or transient. + const noteTypes = { + isSettled: new Fr(0), + isTransient: new Fr(1), + }; + const flattenData = noteDatas.flatMap(({ nonce, note, index }) => [ + nonce, + index === undefined ? noteTypes.isTransient : noteTypes.isSettled, + ...note.items, + ]); + + const returnFieldSize = fromSingle(returnSize).toNumber(); + const returnData = [noteDatas.length, contractAddress, ...flattenData].map(v => new Fr(v)); + if (returnData.length > returnFieldSize) { + throw new Error(`Return data size too big. Maximum ${returnFieldSize} fields. Got ${flattenData.length}.`); + } + + const paddedZeros = Array(returnFieldSize - returnData.length).fill(new Fr(0)); + return toForeignCallResult([toArray([...returnData, ...paddedZeros])]); + } + + notifyCreatedNote( + storageSlot: ForeignCallSingle, + noteTypeId: ForeignCallSingle, + note: ForeignCallArray, + innerNoteHash: ForeignCallSingle, + counter: ForeignCallSingle, + ) { + this.typedOracle.notifyCreatedNote( + fromSingle(storageSlot), + fromSingle(noteTypeId), + fromArray(note), + fromSingle(innerNoteHash), + fromSingle(counter).toNumber(), + ); + return toForeignCallResult([toSingle(new Fr(0))]); + } + + async notifyNullifiedNote( + innerNullifier: ForeignCallSingle, + innerNoteHash: ForeignCallSingle, + counter: ForeignCallSingle, + ) { + await this.typedOracle.notifyNullifiedNote( + fromSingle(innerNullifier), + fromSingle(innerNoteHash), + fromSingle(counter).toNumber(), + ); + return toForeignCallResult([toSingle(new Fr(0))]); + } + + async checkNullifierExists(innerNullifier: ForeignCallSingle) { + const exists = await this.typedOracle.checkNullifierExists(fromSingle(innerNullifier)); + return toForeignCallResult([toSingle(new Fr(exists))]); + } + + async getContractInstance(address: ForeignCallSingle) { + const instance = await this.typedOracle.getContractInstance(fromSingle(address)); + return toForeignCallResult([ + toArray([ + instance.salt, + instance.deployer, + instance.contractClassId, + instance.initializationHash, + instance.publicKeysHash, + ]), + ]); + } + + async avmOpcodeGetContractInstance(address: ForeignCallSingle) { + const instance = await this.typedOracle.getContractInstance(fromSingle(address)); + return toForeignCallResult([ + toArray([ + // AVM requires an extra boolean indicating the instance was found + new Fr(1), + instance.salt, + instance.deployer, + instance.contractClassId, + instance.initializationHash, + instance.publicKeysHash, + ]), + ]); + } + + avmOpcodeSender() { + const sender = (this.typedOracle as TXE).getMsgSender(); + return toForeignCallResult([toSingle(sender)]); + } + + async avmOpcodeEmitNullifier(nullifier: ForeignCallSingle) { + await (this.typedOracle as TXE).avmOpcodeEmitNullifier(fromSingle(nullifier)); + return toForeignCallResult([]); + } + + async avmOpcodeEmitNoteHash(innerNoteHash: ForeignCallSingle) { + await (this.typedOracle as TXE).avmOpcodeEmitNoteHash(fromSingle(innerNoteHash)); + return toForeignCallResult([]); + } + + async avmOpcodeNullifierExists(innerNullifier: ForeignCallSingle, targetAddress: ForeignCallSingle) { + const exists = await (this.typedOracle as TXE).avmOpcodeNullifierExists( + fromSingle(innerNullifier), + AztecAddress.fromField(fromSingle(targetAddress)), + ); + return toForeignCallResult([toSingle(new Fr(exists))]); + } + + async getPublicKeysAndPartialAddress(address: ForeignCallSingle) { + const parsedAddress = AztecAddress.fromField(fromSingle(address)); + const { publicKeys, partialAddress } = await this.typedOracle.getCompleteAddress(parsedAddress); + + return toForeignCallResult([toArray([...publicKeys.toFields(), partialAddress])]); + } + + async getKeyValidationRequest(pkMHash: ForeignCallSingle) { + const keyValidationRequest = await this.typedOracle.getKeyValidationRequest(fromSingle(pkMHash)); + return toForeignCallResult([toArray(keyValidationRequest.toFields())]); + } + + computeEncryptedNoteLog( + contractAddress: ForeignCallSingle, + storageSlot: ForeignCallSingle, + noteTypeId: ForeignCallSingle, + ovskApp: ForeignCallSingle, + ovpkMX: ForeignCallSingle, + ovpkMY: ForeignCallSingle, + ivpkMX: ForeignCallSingle, + ivpkMY: ForeignCallSingle, + preimage: ForeignCallArray, + ) { + const ovpkM = new Point(fromSingle(ovpkMX), fromSingle(ovpkMY)); + const ovKeys = new KeyValidationRequest(ovpkM, Fr.fromString(fromSingle(ovskApp).toString())); + const ivpkM = new Point(fromSingle(ivpkMX), fromSingle(ivpkMY)); + const encLog = this.typedOracle.computeEncryptedNoteLog( + AztecAddress.fromString(fromSingle(contractAddress).toString()), + Fr.fromString(fromSingle(storageSlot).toString()), + Fr.fromString(fromSingle(noteTypeId).toString()), + ovKeys, + ivpkM, + fromArray(preimage), + ); + const bytes: Fr[] = []; + + encLog.forEach(v => { + bytes.push(new Fr(v)); + }); + return toForeignCallResult([toArray(bytes)]); + } + + emitEncryptedLog( + _contractAddress: ForeignCallSingle, + _randomandomness: ForeignCallSingle, + _encryptedLog: ForeignCallSingle, + _counter: ForeignCallSingle, + ) { + return toForeignCallResult([]); + } + + emitEncryptedNoteLog( + _noteHashCounter: ForeignCallSingle, + _encryptedNote: ForeignCallArray, + _counter: ForeignCallSingle, + ) { + return toForeignCallResult([]); + } + + async callPrivateFunction( + targetContractAddress: ForeignCallSingle, + functionSelector: ForeignCallSingle, + argsHash: ForeignCallSingle, + sideEffectCounter: ForeignCallSingle, + isStaticCall: ForeignCallSingle, + isDelegateCall: ForeignCallSingle, + ) { + const result = await this.typedOracle.callPrivateFunction( + fromSingle(targetContractAddress), + FunctionSelector.fromField(fromSingle(functionSelector)), + fromSingle(argsHash), + fromSingle(sideEffectCounter).toNumber(), + fromSingle(isStaticCall).toBool(), + fromSingle(isDelegateCall).toBool(), + ); + return toForeignCallResult([toArray(result.toFields())]); + } + + async getNullifierMembershipWitness(blockNumber: ForeignCallSingle, nullifier: ForeignCallSingle) { + const parsedBlockNumber = fromSingle(blockNumber).toNumber(); + const witness = await this.typedOracle.getNullifierMembershipWitness(parsedBlockNumber, fromSingle(nullifier)); + if (!witness) { + throw new Error(`Nullifier membership witness not found at block ${parsedBlockNumber}.`); + } + return toForeignCallResult([toArray(witness.toFields())]); + } +} diff --git a/yarn-project/txe/src/util/encoding.ts b/yarn-project/txe/src/util/encoding.ts new file mode 100644 index 00000000000..0d4a78ba61a --- /dev/null +++ b/yarn-project/txe/src/util/encoding.ts @@ -0,0 +1,29 @@ +import { Fr } from '@aztec/foundation/fields'; + +export type ForeignCallSingle = string; + +export type ForeignCallArray = string[]; + +export type ForeignCallResult = { + values: (ForeignCallSingle | ForeignCallArray)[]; +}; + +export function fromSingle(obj: ForeignCallSingle) { + return Fr.fromBuffer(Buffer.from(obj, 'hex')); +} + +export function fromArray(obj: ForeignCallArray) { + return obj.map(str => Fr.fromBuffer(Buffer.from(str, 'hex'))); +} + +export function toSingle(obj: Fr) { + return obj.toString().slice(2); +} + +export function toArray(objs: Fr[]) { + return objs.map(obj => obj.toString()); +} + +export function toForeignCallResult(obj: (ForeignCallSingle | ForeignCallArray)[]) { + return { values: obj }; +} diff --git a/yarn-project/txe/src/util/txe_database.ts b/yarn-project/txe/src/util/txe_database.ts new file mode 100644 index 00000000000..b154fd8702a --- /dev/null +++ b/yarn-project/txe/src/util/txe_database.ts @@ -0,0 +1,24 @@ +import { type AztecAddress, CompleteAddress } from '@aztec/circuits.js'; +import { type AztecKVStore, type AztecMap } from '@aztec/kv-store'; +import { KVPxeDatabase } from '@aztec/pxe'; + +export class TXEDatabase extends KVPxeDatabase { + #accounts: AztecMap; + + constructor(db: AztecKVStore) { + super(db); + this.#accounts = db.openMap('accounts'); + } + + getAccount(key: AztecAddress) { + const completeAddress = this.#accounts.get(key.toString()); + if (!completeAddress) { + throw new Error(`Account not found: ${key.toString()}`); + } + return CompleteAddress.fromBuffer(completeAddress); + } + + async setAccount(key: AztecAddress, value: CompleteAddress) { + await this.#accounts.set(key.toString(), value.toBuffer()); + } +} diff --git a/yarn-project/txe/tsconfig.json b/yarn-project/txe/tsconfig.json new file mode 100644 index 00000000000..7db2bf79778 --- /dev/null +++ b/yarn-project/txe/tsconfig.json @@ -0,0 +1,47 @@ +{ + "extends": "..", + "compilerOptions": { + "outDir": "dest", + "rootDir": "src", + "tsBuildInfoFile": ".tsbuildinfo" + }, + "references": [ + { + "path": "../archiver" + }, + { + "path": "../aztec.js" + }, + { + "path": "../circuit-types" + }, + { + "path": "../circuits.js" + }, + { + "path": "../foundation" + }, + { + "path": "../key-store" + }, + { + "path": "../kv-store" + }, + { + "path": "../noir-contracts.js" + }, + { + "path": "../pxe" + }, + { + "path": "../simulator" + }, + { + "path": "../types" + }, + { + "path": "../world-state" + } + ], + "include": ["src"] +} diff --git a/yarn-project/types/src/abi/contract_artifact.ts b/yarn-project/types/src/abi/contract_artifact.ts index 8ca72aa68a7..9fff2b21b6a 100644 --- a/yarn-project/types/src/abi/contract_artifact.ts +++ b/yarn-project/types/src/abi/contract_artifact.ts @@ -2,7 +2,6 @@ import { type ABIParameter, type ABIParameterVisibility, type AbiType, - type BasicValue, type ContractArtifact, type ContractNote, type FieldLayout, @@ -227,10 +226,8 @@ function getStorageLayout(input: NoirCompiledContract) { return storageFields.reduce((acc: Record, field) => { const name = field.name; const slot = field.value.fields[0].value as IntegerValue; - const typ = field.value.fields[1].value as BasicValue<'string', string>; acc[name] = { slot: new Fr(BigInt(slot.value)), - typ: typ.value, }; return acc; }, {}); diff --git a/yarn-project/world-state/src/world-state-db/index.ts b/yarn-project/world-state/src/world-state-db/index.ts index 9d72e0991e8..f4c20a567f9 100644 --- a/yarn-project/world-state/src/world-state-db/index.ts +++ b/yarn-project/world-state/src/world-state-db/index.ts @@ -1,3 +1,5 @@ export * from './merkle_trees.js'; export * from './merkle_tree_db.js'; export * from './merkle_tree_operations.js'; +export * from './merkle_tree_operations_facade.js'; +export * from './merkle_tree_snapshot_operations_facade.js'; diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index d1fe44fd9df..cc328c7abd1 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -933,6 +933,34 @@ __metadata: languageName: unknown linkType: soft +"@aztec/txe@workspace:txe": + version: 0.0.0-use.local + resolution: "@aztec/txe@workspace:txe" + dependencies: + "@aztec/archiver": "workspace:^" + "@aztec/aztec.js": "workspace:^" + "@aztec/circuit-types": "workspace:^" + "@aztec/circuits.js": "workspace:^" + "@aztec/foundation": "workspace:^" + "@aztec/key-store": "workspace:^" + "@aztec/kv-store": "workspace:^" + "@aztec/noir-contracts.js": "workspace:^" + "@aztec/pxe": "workspace:^" + "@aztec/simulator": "workspace:^" + "@aztec/types": "workspace:^" + "@aztec/world-state": "workspace:^" + "@jest/globals": ^29.5.0 + "@types/jest": ^29.5.0 + "@types/node": ^18.7.23 + jest: ^29.5.0 + jest-mock-extended: ^3.0.3 + ts-node: ^10.9.1 + typescript: ^5.0.4 + bin: + txe: ./dest/bin/index.js + languageName: unknown + linkType: soft + "@aztec/types@workspace:^, @aztec/types@workspace:types": version: 0.0.0-use.local resolution: "@aztec/types@workspace:types"