diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 671ce7a2acbc..196a9efb59a8 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 a8ab844ebe13..86a0427f7901 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 ab4c6270e97b..b8abf566f844 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 2a7c32ab568e..df8db2aa0469 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 69904595c964..dd1374f9eb01 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 7229268f1e55..9ee0738312ef 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 aca21ab515a1..c3949a8e03ea 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 000000000000..7614ffff1d10 --- /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 6403f166e9e8..b193640a54b2 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 0f8cce2323cc..8ae9faf228c4 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 c5c7a4b5f311..b28a85add1cb 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 000000000000..014757cf9b00 --- /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 0b04ac52ccbd..000000000000 --- 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 000000000000..f4c53c95c20c --- /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 000000000000..142f6fd58f8f --- /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 000000000000..7baec3523d8b --- /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 fb8ef27c7bf9..148cc284a9f0 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 000000000000..1a0ed1a304ee --- /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 43bfe0110639..6a34f458cce0 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 cf2a235e7987..e296d211aabc 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 53a4d6145648..a91d3bd758c9 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 fb066de5ad6d..e6901b1a3191 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 c68563ec1b1b..79046b74aa0d 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 4c6db0c5d4d6..fca6ed10b8f3 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 d00374bb4e47..3bf3f00fb1bc 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 bce2078479a6..01af1206fecc 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 a36b7b17d097..d79c7b190ed0 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 90f9ce6164a7..1875ab0b2521 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 a1c21c7efcf3..bac87502c7d1 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 7ecea5c9eac5..6dfd575d2b22 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 62ead1f534f3..64d984c8cf99 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 14b724a28b56..f39379e20a18 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 a7294537624a..81aa8727e172 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 614cc21e631c..2a51d433bc94 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 1d45791cdab0..3168be0ce0c7 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 f8d18abdac76..b7154d1226c6 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 200e7b0088c9..2b6080866042 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 fa08f05b063d..388c8f4d6df4 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 4685cc2f7a8f..e01c18032417 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 d7cf6d57253a..7c62b24d3ff1 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 325e0b8f80b1..3615b012c99a 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 60fcf15d4a98..bb30eff13125 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 000000000000..e659927475c0 --- /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 000000000000..5658cd3b454e --- /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 000000000000..149347621599 --- /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 000000000000..09115f3ebab2 --- /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 000000000000..6b53c0de1ebd --- /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 000000000000..0d4a78ba61ad --- /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 000000000000..b154fd8702a2 --- /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 000000000000..7db2bf797787 --- /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 8ca72aa68a75..9fff2b21b6a4 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 9d72e0991e84..f4c20a567f9e 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 d1fe44fd9dfd..cc328c7abd12 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"