diff --git a/noir-projects/Dockerfile.test b/noir-projects/Dockerfile.test index 40edcbaaf35..91adc723c6a 100644 --- a/noir-projects/Dockerfile.test +++ b/noir-projects/Dockerfile.test @@ -28,7 +28,9 @@ RUN cd /usr/src/yarn-project/txe && yarn start & echo $! > /tmp/txe.pid && \ # Wait for TXE to initialize sleep 5 && \ cd ./noir-contracts && \ - ./bootstrap.sh && nargo test --silence-warnings --oracle-resolver http://localhost:8080 ; \ + # We need to increase the timeout since all tests running in parallel hammer TXE at the same time, and processing slows down leading to timeouts + # The only way we currently have to batch tests is via RAYON_NUM_THREADS, which is not ideal + ./bootstrap.sh && NARGO_FOREIGN_CALL_TIMEOUT=300000 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 && \ diff --git a/noir-projects/Earthfile b/noir-projects/Earthfile index df8db2aa046..a828544fea2 100644 --- a/noir-projects/Earthfile +++ b/noir-projects/Earthfile @@ -58,7 +58,10 @@ test: 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 ; \ + cd /usr/src/noir-projects/noir-contracts && \ + # We need to increase the timeout since all tests running in parallel hammer TXE at the same time and processing slows down, leading to timeouts + # The only way we currently have to batch tests is via RAYON_NUM_THREADS, which is not ideal + NARGO_FOREIGN_CALL_TIMEOUT=300000 nargo test --silence-warnings --oracle-resolver http://localhost:8080 ; \ kill $(cat /tmp/txe.pid) format: diff --git a/noir-projects/aztec-nr/authwit/src/cheatcodes.nr b/noir-projects/aztec-nr/authwit/src/cheatcodes.nr new file mode 100644 index 00000000000..f673a203277 --- /dev/null +++ b/noir-projects/aztec-nr/authwit/src/cheatcodes.nr @@ -0,0 +1,44 @@ +use dep::aztec::{ + protocol_types::address::AztecAddress, + context::{public_context::PublicContext, call_interfaces::CallInterface}, test::helpers::cheatcodes, + hash::hash_args +}; + +use crate::auth::{compute_inner_authwit_hash, compute_outer_authwit_hash, set_authorized}; + +pub fn add_private_authwit_from_call_interface( + on_behalf_of: AztecAddress, + caller: AztecAddress, + call_interface: C +) where C: CallInterface { + let target = call_interface.get_contract_address(); + let inputs = cheatcodes::get_private_context_inputs(cheatcodes::get_block_number()); + let chain_id = inputs.tx_context.chain_id; + let version = inputs.tx_context.version; + let args_hash = hash_args(call_interface.get_args()); + let selector = call_interface.get_selector(); + let inner_hash = compute_inner_authwit_hash([caller.to_field(), selector.to_field(), args_hash]); + let message_hash = compute_outer_authwit_hash(target, chain_id, version, inner_hash); + cheatcodes::add_authwit(on_behalf_of, message_hash); +} + +pub fn add_public_authwit_from_call_interface( + on_behalf_of: AztecAddress, + caller: AztecAddress, + call_interface: C +) where C: CallInterface { + let current_contract = cheatcodes::get_contract_address(); + cheatcodes::set_contract_address(on_behalf_of); + let target = call_interface.get_contract_address(); + let inputs = cheatcodes::get_private_context_inputs(cheatcodes::get_block_number()); + let chain_id = inputs.tx_context.chain_id; + let version = inputs.tx_context.version; + let args_hash = hash_args(call_interface.get_args()); + let selector = call_interface.get_selector(); + let inner_hash = compute_inner_authwit_hash([caller.to_field(), selector.to_field(), args_hash]); + let message_hash = compute_outer_authwit_hash(target, chain_id, version, inner_hash); + let mut inputs = cheatcodes::get_public_context_inputs(); + let mut context = PublicContext::new(inputs); + set_authorized(&mut context, message_hash, true); + cheatcodes::set_contract_address(current_contract); +} diff --git a/noir-projects/aztec-nr/authwit/src/lib.nr b/noir-projects/aztec-nr/authwit/src/lib.nr index e56460fd701..c4d792a4a26 100644 --- a/noir-projects/aztec-nr/authwit/src/lib.nr +++ b/noir-projects/aztec-nr/authwit/src/lib.nr @@ -2,3 +2,4 @@ mod account; mod auth_witness; mod auth; mod entrypoint; +mod cheatcodes; 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 dd1374f9eb0..35151d1427d 100644 --- a/noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr +++ b/noir-projects/aztec-nr/aztec/src/context/call_interfaces.nr @@ -16,6 +16,7 @@ trait CallInterface { fn get_selector(self) -> FunctionSelector; fn get_name(self) -> str; fn get_contract_address(self) -> AztecAddress; + fn get_is_static(self) -> bool; } impl CallInterface for PrivateCallInterface { @@ -38,6 +39,10 @@ impl CallInterface AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PrivateCallInterface { @@ -46,7 +51,8 @@ struct PrivateCallInterface { name: str, args_hash: Field, args: [Field], - original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs, + is_static: bool } impl PrivateCallInterface { @@ -93,6 +99,10 @@ impl CallInterface AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PrivateVoidCallInterface { @@ -101,7 +111,8 @@ struct PrivateVoidCallInterface { name: str, args_hash: Field, args: [Field], - original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs, + is_static: bool } impl PrivateVoidCallInterface { @@ -144,6 +155,10 @@ impl CallInterface AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PrivateStaticCallInterface { @@ -152,7 +167,8 @@ struct PrivateStaticCallInterface { name: str, args_hash: Field, args: [Field], - original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs, + is_static: bool } impl PrivateStaticCallInterface { @@ -182,6 +198,10 @@ impl CallInterface AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PrivateStaticVoidCallInterface { @@ -190,7 +210,8 @@ struct PrivateStaticVoidCallInterface { name: str, args_hash: Field, args: [Field], - original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs + original: fn[Env](PrivateContextInputs) -> PrivateCircuitPublicInputs, + is_static: bool } impl PrivateStaticVoidCallInterface { @@ -219,6 +240,10 @@ impl CallInterface for PublicCallI fn get_contract_address(self) -> AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PublicCallInterface { @@ -227,7 +252,8 @@ struct PublicCallInterface { name: str, args: [Field], gas_opts: GasOpts, - original: fn[Env](PublicContextInputs) -> T + original: fn[Env](PublicContextInputs) -> T, + is_static: bool } impl PublicCallInterface { @@ -308,6 +334,10 @@ impl CallInterface for PublicVoid fn get_contract_address(self) -> AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PublicVoidCallInterface { @@ -316,7 +346,8 @@ struct PublicVoidCallInterface { name: str, args: [Field], gas_opts: GasOpts, - original: fn[Env](PublicContextInputs) -> () + original: fn[Env](PublicContextInputs) -> (), + is_static: bool } impl PublicVoidCallInterface { @@ -378,7 +409,7 @@ impl PublicVoidCallInterface { } impl CallInterface for PublicStaticCallInterface { - fn get_args(self) -> [Field] { + fn get_args(self) -> [Field] { self.args } @@ -397,6 +428,10 @@ impl CallInterface for PublicStati fn get_contract_address(self) -> AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PublicStaticCallInterface { @@ -405,7 +440,8 @@ struct PublicStaticCallInterface { name: str, args: [Field], gas_opts: GasOpts, - original: fn[Env](PublicContextInputs) -> T + original: fn[Env](PublicContextInputs) -> T, + is_static: bool } impl PublicStaticCallInterface { @@ -453,6 +489,10 @@ impl CallInterface for PublicStat fn get_contract_address(self) -> AztecAddress { self.target_contract } + + fn get_is_static(self) -> bool { + self.is_static + } } struct PublicStaticVoidCallInterface { @@ -461,7 +501,8 @@ struct PublicStaticVoidCallInterface { name: str, args: [Field], gas_opts: GasOpts, - original: fn[Env](PublicContextInputs) -> () + original: fn[Env](PublicContextInputs) -> (), + is_static: bool } impl PublicStaticVoidCallInterface { diff --git a/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr b/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr index 4a7a3a95e94..7fe6021326a 100644 --- a/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr +++ b/noir-projects/aztec-nr/aztec/src/note/lifecycle.nr @@ -15,12 +15,10 @@ pub fn create_note( let note_hash_counter = context.side_effect_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); + note.set_header(header); let inner_note_hash = compute_inner_note_hash(*note); - // TODO: Strong typing required because of https://github.com/noir-lang/noir/issues/4088 - let serialized_note: [Field; N] = Note::serialize_content(*note); + let serialized_note = Note::serialize_content(*note); assert( notify_created_note( storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 42c6bcdb7ee..4d7aad6f6e2 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -145,8 +145,7 @@ unconstrained pub fn get_notes( let header = NoteHeader { contract_address, nonce, storage_slot, note_hash_counter }; let serialized_note = arr_copy_slice(fields, [0; N], read_offset + 2); let mut note = Note::deserialize_content(serialized_note); - // TODO: change this to note.set_header(header) once https://github.com/noir-lang/noir/issues/4095 is fixed - Note::set_header(&mut note, header); + note.set_header(header); placeholder_opt_notes[i] = Option::some(note); }; } diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers.nr b/noir-projects/aztec-nr/aztec/src/test/helpers.nr index b28a85add1c..b7164a82359 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers.nr @@ -1,4 +1,4 @@ mod test_environment; mod cheatcodes; -mod types; +mod utils; 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 index 014757cf9b0..db5e13ed424 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/cheatcodes.nr @@ -1,6 +1,9 @@ -use dep::protocol_types::{abis::function_selector::FunctionSelector, address::{AztecAddress, PartialAddress}}; +use dep::protocol_types::{ + abis::function_selector::FunctionSelector, address::{AztecAddress, PartialAddress}, + constants::CONTRACT_INSTANCE_LENGTH, contract_instance::ContractInstance +}; use crate::context::inputs::{PublicContextInputs, PrivateContextInputs}; -use crate::test::helpers::types::{Deployer, TestAccount}; +use crate::test::helpers::utils::{Deployer, TestAccount}; use crate::keys::public_keys::PublicKeys; unconstrained pub fn reset() { @@ -19,8 +22,8 @@ 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 advance_blocks_by(blocks: u32) { + oracle_advance_blocks_by(blocks); } unconstrained pub fn get_private_context_inputs(historical_block_number: u32) -> PrivateContextInputs { @@ -31,20 +34,12 @@ 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 deploy(path: str, initializer: str, args: [Field], public_keys_hash: Field) -> ContractInstance { + let instance_fields = oracle_deploy(path, initializer, args, public_keys_hash); + ContractInstance::deserialize(instance_fields) } -unconstrained pub fn direct_storage_write( - contract_address: AztecAddress, - storage_slot: Field, - fields: [Field; N] -) { +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); } @@ -72,6 +67,40 @@ unconstrained pub fn get_side_effects_counter() -> u32 { oracle_get_side_effects_counter() } +unconstrained pub fn add_authwit(address: AztecAddress, message_hash: Field) { + orable_add_authwit(address, message_hash) +} + +unconstrained pub fn assert_public_call_fails(target_address: AztecAddress, function_selector: FunctionSelector, args: [Field]) { + oracle_assert_public_call_fails(target_address, function_selector, args) +} + +unconstrained pub fn assert_private_call_fails( + target_address: AztecAddress, + function_selector: FunctionSelector, + argsHash: Field, + sideEffectsCounter: Field, + isStaticCall: bool, + isDelegateCall: bool +) { + oracle_assert_private_call_fails( + target_address, + function_selector, + argsHash, + sideEffectsCounter, + isStaticCall, + isDelegateCall + ) +} + +unconstrained pub fn add_nullifiers(contractAddress: AztecAddress, nullifiers: [Field]) { + oracle_add_nullifiers(contractAddress, nullifiers) +} + +unconstrained pub fn add_note_hashes(contractAddress: AztecAddress, inner_note_hashes: [Field]) { + oracle_add_note_hashes(contractAddress, inner_note_hashes) +} + #[oracle(reset)] fn oracle_reset() {} @@ -84,8 +113,8 @@ fn oracle_set_contract_address(address: AztecAddress) {} #[oracle(getBlockNumber)] fn oracle_get_block_number() -> u32 {} -#[oracle(timeTravel)] -fn oracle_time_travel(blocks: u32) {} +#[oracle(advanceBlocksBy)] +fn oracle_advance_blocks_by(blocks: u32) {} #[oracle(getPrivateContextInputs)] fn oracle_get_private_context_inputs(historical_block_number: u32) -> PrivateContextInputs {} @@ -99,7 +128,7 @@ fn oracle_deploy( initializer: str, args: [Field], public_keys_hash: Field -) -> AztecAddress {} +) -> [Field; CONTRACT_INSTANCE_LENGTH] {} #[oracle(directStorageWrite)] fn direct_storage_write_oracle( @@ -125,3 +154,30 @@ fn oracle_set_msg_sender(msg_sender: AztecAddress) {} #[oracle(getSideEffectsCounter)] fn oracle_get_side_effects_counter() -> u32 {} + +#[oracle(addAuthWitness)] +fn orable_add_authwit(address: AztecAddress, message_hash: Field) {} + +#[oracle(assertPublicCallFails)] +fn oracle_assert_public_call_fails( + target_address: AztecAddress, + function_selector: FunctionSelector, + args: [Field] +) {} + +#[oracle(assertPrivateCallFails)] +fn oracle_assert_private_call_fails( + target_address: AztecAddress, + function_selector: FunctionSelector, + argsHash: Field, + sideEffectsCounter: Field, + isStaticCall: bool, + isDelegateCall: bool +) {} + +#[oracle(addNullifiers)] +fn oracle_add_nullifiers(contractAddress: AztecAddress, nullifiers: [Field]) {} + +#[oracle(addNoteHashes)] +fn oracle_add_note_hashes(contractAddress: AztecAddress, inner_note_hashes: [Field]) {} + 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 index 4f2800b19fc..9b66e64264b 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment.nr @@ -8,9 +8,9 @@ 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::test::helpers::{cheatcodes, utils::{apply_side_effects_private, Deployer, TestAccount}, keys}; use crate::keys::constants::{NULLIFIER_INDEX, INCOMING_INDEX, OUTGOING_INDEX, TAGGING_INDEX}; -use crate::hash::hash_args; +use crate::hash::{hash_args, hash_args_array}; use crate::note::{ note_header::NoteHeader, note_interface::NoteInterface, @@ -18,16 +18,12 @@ use crate::note::{ }; use crate::oracle::notes::notify_created_note; -struct TestEnvironment { - contract_address: Option, - args_hash: Option, - function_selector: Option -} +struct TestEnvironment {} impl TestEnvironment { fn new() -> Self { cheatcodes::reset(); - Self { contract_address: Option::none(), args_hash: Option::none(), function_selector: Option::none() } + Self {} } fn block_number(self) -> u32 { @@ -40,7 +36,7 @@ impl TestEnvironment { } fn advance_block_by(&mut self, blocks: u32) { - cheatcodes::advance_blocks(blocks); + cheatcodes::advance_blocks_by(blocks); } fn public(self) -> PublicContext { @@ -74,26 +70,41 @@ impl TestEnvironment { test_account.address } - fn create_account_contract(self, secret: Field) -> AztecAddress { + fn create_account_contract(&mut 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( + let args = [public_keys.ivpk_m.x, public_keys.ivpk_m.y]; + let instance = cheatcodes::deploy( "@aztec/noir-contracts.js/SchnorrAccount", "constructor", - args, + args.as_slice(), 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; + cheatcodes::advance_blocks_by(1); + let test_account = cheatcodes::add_account( + secret, + PartialAddress::compute( + instance.contract_class_id, + instance.salt, + instance.initialization_hash, + instance.deployer + ) + ); let keys = test_account.keys; + let address = instance.to_address(); + 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 + let selector = FunctionSelector::from_signature("constructor(Field,Field)"); + + let mut context = self.private_at(cheatcodes::get_block_number()); + + let _ = context.call_private_function(address, selector, args); + + address } fn deploy(self, path: str) -> Deployer { @@ -113,7 +124,9 @@ impl TestEnvironment { 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(); + inputs.call_context.is_static_call = call_interface.get_is_static(); let public_inputs = original_fn(inputs); + apply_side_effects_private(target_address, public_inputs); cheatcodes::set_contract_address(original_contract_address); cheatcodes::set_msg_sender(original_msg_sender); @@ -133,7 +146,9 @@ impl TestEnvironment { 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(); + inputs.call_context.is_static_call = call_interface.get_is_static(); let public_inputs = original_fn(inputs); + apply_side_effects_private(target_address, public_inputs); cheatcodes::set_contract_address(original_contract_address); cheatcodes::set_msg_sender(original_msg_sender); @@ -151,6 +166,7 @@ impl TestEnvironment { 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()); + inputs.is_static_call = call_interface.get_is_static(); let result = original_fn(inputs); cheatcodes::set_contract_address(original_contract_address); @@ -158,21 +174,23 @@ impl TestEnvironment { 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); + fn assert_public_call_fails(self, call_interface: C) where C: CallInterface { + cheatcodes::assert_public_call_fails( + call_interface.get_contract_address(), + call_interface.get_selector(), + call_interface.get_args() + ); + } - cheatcodes::set_contract_address(original_contract_address); - cheatcodes::set_msg_sender(original_msg_sender); + fn assert_private_call_fails(self, call_interface: C) where C: CallInterface { + cheatcodes::assert_private_call_fails( + call_interface.get_contract_address(), + call_interface.get_selector(), + hash_args(call_interface.get_args()), + cheatcodes::get_side_effects_counter() as Field, + call_interface.get_is_static(), + false + ); } pub fn store_note_in_cache( @@ -186,12 +204,9 @@ impl TestEnvironment { 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); + note.set_header(header); let inner_note_hash = compute_inner_note_hash(*note); - - // TODO: Strong typing required because of https://github.com/noir-lang/noir/issues/4088 - let serialized_note: [Field; N] = Note::serialize_content(*note); + let serialized_note = Note::serialize_content(*note); assert( notify_created_note( storage_slot, diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/types.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/utils.nr similarity index 67% rename from noir-projects/aztec-nr/aztec/src/test/helpers/types.nr rename to noir-projects/aztec-nr/aztec/src/test/helpers/utils.nr index 7baec3523d8..808b5ad37f5 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/types.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/utils.nr @@ -1,6 +1,7 @@ use dep::protocol_types::{ traits::{Deserialize, Serialize}, address::AztecAddress, - abis::{function_selector::FunctionSelector, private_circuit_public_inputs::PrivateCircuitPublicInputs} + abis::{function_selector::FunctionSelector, private_circuit_public_inputs::PrivateCircuitPublicInputs}, + contract_instance::ContractInstance }; use crate::context::inputs::{PublicContextInputs, PrivateContextInputs}; @@ -9,6 +10,25 @@ use crate::test::helpers::cheatcodes; use crate::keys::public_keys::{PUBLIC_KEYS_LENGTH, PublicKeys}; use crate::hash::hash_args; +use crate::oracle::notes::notify_nullified_note; + +pub fn apply_side_effects_private(contract_address: AztecAddress, public_inputs: PrivateCircuitPublicInputs) { + let mut nullifiers = &[]; + for nullifier in public_inputs.new_nullifiers { + if nullifier.value != 0 { + nullifiers = nullifiers.push_back(nullifier.value); + } + } + cheatcodes::add_nullifiers(contract_address, nullifiers); + let mut note_hashes = &[]; + for note_hash in public_inputs.new_note_hashes { + if note_hash.value != 0 { + note_hashes = note_hashes.push_back(note_hash.value); + } + } + cheatcodes::add_note_hashes(contract_address, note_hashes); +} + struct Deployer { path: str, public_keys_hash: Field @@ -18,14 +38,15 @@ impl Deployer { pub fn with_private_initializer( self, call_interface: C - ) -> AztecAddress where C: CallInterface { - let address = cheatcodes::deploy( + ) -> ContractInstance where C: CallInterface { + let instance = cheatcodes::deploy( self.path, call_interface.get_name(), call_interface.get_args(), self.public_keys_hash ); - cheatcodes::advance_blocks(1); + let address = instance.to_address(); + cheatcodes::advance_blocks_by(1); let block_number = cheatcodes::get_block_number(); let original_fn = call_interface.get_original(); let original_msg_sender = cheatcodes::get_msg_sender(); @@ -35,29 +56,30 @@ impl Deployer { 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); - + let public_inputs = original_fn(inputs); + apply_side_effects_private(address, public_inputs); + cheatcodes::advance_blocks_by(1); cheatcodes::set_contract_address(original_contract_address); cheatcodes::set_msg_sender(original_msg_sender); - address + instance } pub fn with_public_initializer( self, call_interface: C - ) -> AztecAddress where C: CallInterface { - let address = cheatcodes::deploy( + ) -> ContractInstance where C: CallInterface { + let instance = cheatcodes::deploy( self.path, call_interface.get_name(), call_interface.get_args(), self.public_keys_hash ); - cheatcodes::advance_blocks(1); + cheatcodes::advance_blocks_by(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_contract_address(instance.to_address()); cheatcodes::set_msg_sender(original_contract_address); let mut inputs = cheatcodes::get_public_context_inputs(); inputs.selector = call_interface.get_selector().to_field(); @@ -66,12 +88,11 @@ impl Deployer { cheatcodes::set_contract_address(original_contract_address); cheatcodes::set_msg_sender(original_msg_sender); - address + instance } - pub fn without_initializer(self) -> AztecAddress { - let address = cheatcodes::deploy(self.path, "", &[], self.public_keys_hash); - address + pub fn without_initializer(self) -> ContractInstance { + cheatcodes::deploy(self.path, "", &[], self.public_keys_hash) } } 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 b843313be4b..27631ddbe72 100644 --- a/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/counter_contract/src/main.nr @@ -44,15 +44,19 @@ contract Counter { use dep::aztec::note::note_viewer_options::NoteViewerOptions; #[test] - fn test_initialize() { + fn test_increment() { // Setup env, generate keys let mut env = TestEnvironment::new(); let owner = env.create_account(); let outgoing_viewer = env.create_account(); + let initial_value: Field = 5; + cheatcodes::set_contract_address(owner); // 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); + let initializer = Counter::interface().initialize(initial_value as u64, owner, outgoing_viewer); + let counter_contract = env.deploy("@aztec/noir-contracts.js/Counter").with_private_initializer(initializer); + let contract_address = counter_contract.to_address(); + // Read the stored value in the note cheatcodes::set_contract_address(contract_address); @@ -60,6 +64,18 @@ contract Counter { let owner_slot = derive_storage_slot_in_map(counter_slot, owner); let mut options = NoteViewerOptions::new(); let notes: BoundedVec = view_notes(owner_slot, options); - assert(notes.get(0).value == 5); + let initial_note_value = notes.get(0).value; + assert( + initial_note_value == initial_value, f"Expected {initial_value} but got {initial_note_value}" + ); + + // Increment the counter + let increment_call_interface = Counter::at(contract_address).increment(owner, outgoing_viewer); + env.call_private_void(increment_call_interface); + let current_value_for_owner = get_counter(owner); + let expected_current_value = initial_value + 1; + assert( + expected_current_value == current_value_for_owner, f"Expected {expected_current_value} but got {current_value_for_owner}" + ); } } 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 b8789b55e6f..efeae7bcda8 100644 --- a/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/parent_contract/src/main.nr @@ -257,8 +257,9 @@ contract Parent { 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); + let child_contract = env.deploy("@aztec/noir-contracts.js/Child").without_initializer(); + let child_contract_address = child_contract.to_address(); + cheatcodes::advance_blocks_by(1); // Set value in child through parent let value_to_set = 7; 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 36390dda0be..2cea5e637f2 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -1,6 +1,7 @@ // docs:start:token_all // docs:start:imports mod types; +mod test; // Minimal token implementation that supports `AuthWit` accounts. // The auth message follows a similar pattern to the cross-chain message and includes a designated caller. @@ -406,75 +407,5 @@ 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 notes: BoundedVec = view_notes(recipient_slot, options); - assert(notes.get(0).amount.to_field() == transfer_amount); - - let owner_slot = derive_storage_slot_in_map(balances_slot, owner); - let mut options = NoteViewerOptions::new(); - let notes: BoundedVec = view_notes(owner_slot, options); - assert(notes.get(0).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/token_contract/src/test.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr new file mode 100644 index 00000000000..cf797ce3bcc --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test.nr @@ -0,0 +1,9 @@ +mod access_control; +mod burn; +mod utils; +mod transfer_public; +mod transfer_private; +mod unshielding; +mod minting; +mod reading_constants; +mod shielding; diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr new file mode 100644 index 00000000000..37a84e09a7b --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/access_control.nr @@ -0,0 +1,52 @@ +use crate::test::utils; +use dep::aztec::test::helpers::cheatcodes; +use crate::Token; + +#[test] +unconstrained fn access_control() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient) = utils::setup(/* with_account_contracts */ false); + + // Set a new admin + let set_admin_call_interface = Token::at(token_contract_address).set_admin(recipient); + env.call_public(set_admin_call_interface); + + // Check it worked + let get_admin_call_interface = Token::at(token_contract_address).admin(); + let admin = env.call_public(get_admin_call_interface); + assert(admin == recipient.to_field()); + + // Impersonate new admin + cheatcodes::set_contract_address(recipient); + + // Check new admin is not a minter + let is_minter_call_interface = Token::at(token_contract_address).is_minter(recipient); + let is_minter = env.call_public(is_minter_call_interface); + assert(is_minter == false); + // Set admin as minter + let set_minter_call_interface = Token::at(token_contract_address).set_minter(recipient, true); + env.call_public(set_minter_call_interface); + + // Check it worked + let is_minter = env.call_public(is_minter_call_interface); + assert(is_minter == true); + + // Revoke minter as admin + let set_minter_call_interface = Token::at(token_contract_address).set_minter(recipient, false); + env.call_public(set_minter_call_interface); + + // Check it worked + let is_minter = env.call_public(is_minter_call_interface); + assert(is_minter == false); + + // Impersonate original admin + cheatcodes::set_contract_address(owner); + + // Try to set ourselves as admin, fail miserably + let set_admin_call_interface = Token::at(token_contract_address).set_admin(recipient); + env.assert_public_call_fails(set_admin_call_interface); + + // Try to revoke minter status to recipient, fail miserably + let set_minter_call_interface = Token::at(token_contract_address).set_minter(recipient, false); + env.assert_public_call_fails(set_minter_call_interface); +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr new file mode 100644 index 00000000000..af0e6cb3c31 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/burn.nr @@ -0,0 +1,179 @@ +use crate::test::utils; +use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand}; +use dep::authwit::cheatcodes as authwit_cheatcodes; +use crate::Token; + +#[test] +unconstrained fn burn_public_success() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + let burn_amount = mint_amount / 10; + + // Burn less than balance + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, 0); + env.call_public(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount - burn_amount); +} + +#[test] +unconstrained fn burn_public_on_behalf_of_other() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let burn_amount = mint_amount / 10; + + // Burn on behalf of other + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, unsafe_rand()); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, recipient, burn_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Burn tokens + env.call_public(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount - burn_amount); +} + +#[test] +unconstrained fn burn_public_failure_more_than_balance() { + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + // Burn more than balance + let burn_amount = mint_amount * 10; + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, 0); + env.assert_public_call_fails(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn burn_public_failure_on_behalf_of_self_non_zero_nonce() { + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + // Burn on behalf of self with non-zero nonce + let burn_amount = mint_amount / 10; + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, unsafe_rand()); + env.assert_public_call_fails(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn burn_public_failure_on_behalf_of_other_without_approval() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + // Burn on behalf of other without approval + let burn_amount = mint_amount / 10; + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, unsafe_rand()); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + env.assert_public_call_fails(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount); + + // Burn on behalf of other, wrong designated caller + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, unsafe_rand()); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, owner, burn_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + env.assert_public_call_fails(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn burn_public_failure_on_behalf_of_other_wrong_caller() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + // Burn on behalf of other, wrong designated caller + let burn_amount = mint_amount / 10; + let burn_call_interface = Token::at(token_contract_address).burn_public(owner, burn_amount, unsafe_rand()); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, owner, burn_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + env.assert_public_call_fails(burn_call_interface); + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn burn_private_on_behalf_of_self() { + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + let burn_amount = mint_amount / 10; + + // Burn less than balance + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, 0); + env.call_private_void(burn_call_interface); + utils::check_private_balance(token_contract_address, owner, mint_amount - burn_amount); +} + +#[test] +unconstrained fn burn_private_on_behalf_of_other() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let burn_amount = mint_amount / 10; + + // Burn on behalf of other + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, burn_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Burn tokens + env.call_private_void(burn_call_interface); + utils::check_private_balance(token_contract_address, owner, mint_amount - burn_amount); +} + +#[test(should_fail_with="Balance too low")] +unconstrained fn burn_private_failure_more_than_balance() { + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + // Burn more than balance + let burn_amount = mint_amount * 10; + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, 0); + env.call_private_void(burn_call_interface); + // Private doesnt revert, so we cannot check balances here since notes have already been nullified. Test is done. +} + +#[test(should_fail_with="invalid nonce")] +unconstrained fn burn_private_failure_on_behalf_of_self_non_zero_nonce() { + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + // Burn more than balance + let burn_amount = mint_amount / 10; + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); + env.call_private_void(burn_call_interface); + // Private doesnt revert, so we cannot check balances here since notes have already been nullified. Test is done. +} + +#[test(should_fail)] +unconstrained fn burn_private_failure_on_behalf_of_other_more_than_balance() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + // Burn more than balance + let burn_amount = mint_amount * 10; + // Burn on behalf of other + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, burn_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + env.call_private_void(burn_call_interface); + // Private doesnt revert, so we cannot check balances here since notes have already been nullified. Test is done. +} + +#[test(should_fail)] +unconstrained fn burn_private_failure_on_behalf_of_other_without_approval() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + // Burn more than balance + let burn_amount = mint_amount / 10; + // Burn on behalf of other + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + env.call_private_void(burn_call_interface); + // Private doesnt revert, so we cannot check balances here since notes have already been nullified. Test is done. +} + +#[test(should_fail)] +unconstrained fn burn_private_failure_on_behalf_of_other_wrong_designated_caller() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + // Burn more than balance + let burn_amount = mint_amount / 10; + // Burn on behalf of other + let burn_call_interface = Token::at(token_contract_address).burn(owner, burn_amount, unsafe_rand()); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, owner, burn_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + env.call_private_void(burn_call_interface); + // Private doesnt revert, so we cannot check balances here since notes have already been nullified. Test is done. +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/minting.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/minting.nr new file mode 100644 index 00000000000..4e92489a59a --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/minting.nr @@ -0,0 +1,239 @@ +use crate::test::utils; +use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, hash::compute_secret_hash}; +use crate::{types::transparent_note::TransparentNote, Token}; + +#[test] +unconstrained fn mint_public_success() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _) = utils::setup(/* with_account_contracts */ false); + + let mint_amount = 10000; + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.call_public(mint_public_call_interface); + + utils::check_public_balance(token_contract_address, owner, mint_amount); + + let total_supply_call_interface = Token::at(token_contract_address).total_supply(); + let total_supply = env.call_public(total_supply_call_interface); + + assert(total_supply == mint_amount); +} + +#[test] +unconstrained fn mint_public_failures() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient) = utils::setup(/* with_account_contracts */ false); + + // As non-minter + let mint_amount = 10000; + cheatcodes::set_contract_address(recipient); + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.assert_public_call_fails(mint_public_call_interface); + + utils::check_public_balance(token_contract_address, owner, 0); + + cheatcodes::set_contract_address(owner); + + // Overflow recipient + + let mint_amount = 2.pow_32(128); + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.assert_public_call_fails(mint_public_call_interface); + + utils::check_public_balance(token_contract_address, owner, 0); + + // Overflow total supply + + let mint_for_recipient_amount = 1000; + + let mint_public_call_interface = Token::at(token_contract_address).mint_public(recipient, mint_for_recipient_amount); + env.call_public(mint_public_call_interface); + + let mint_amount = 2.pow_32(128) - mint_for_recipient_amount; + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.assert_public_call_fails(mint_public_call_interface); + + utils::check_public_balance(token_contract_address, recipient, mint_for_recipient_amount); + utils::check_public_balance(token_contract_address, owner, 0); +} + +#[test] +unconstrained fn mint_private_success() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _) = utils::setup(/* with_account_contracts */ false); + let mint_amount = 10000; + // Mint some tokens + let secret = unsafe_rand(); + 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); + + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.call_public(mint_public_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); + + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + +#[test(should_fail_with="Cannot return zero notes")] +unconstrained fn mint_private_failure_double_spend() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient) = utils::setup(/* with_account_contracts */ false); + let mint_amount = 10000; + // Mint some tokens + let secret = unsafe_rand(); + 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); + + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.call_public(mint_public_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); + + utils::check_private_balance(token_contract_address, owner, mint_amount); + + // Attempt to double spend + let redeem_shield_call_interface = Token::at(token_contract_address).redeem_shield(recipient, mint_amount, secret); + env.call_private_void(redeem_shield_call_interface); +} + +#[test(should_fail_with="caller is not minter")] +unconstrained fn mint_private_failure_non_minter() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, _, recipient) = utils::setup(/* with_account_contracts */ false); + let mint_amount = 10000; + // Try to mint some tokens impersonating recipient + cheatcodes::set_contract_address(recipient); + + let secret = unsafe_rand(); + 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); +} + +#[test(should_fail_with="call to assert_max_bit_size")] +unconstrained fn mint_private_failure_overflow() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, _, _) = utils::setup(/* with_account_contracts */ false); + + // Overflow recipient + let mint_amount = 2.pow_32(128); + let secret = unsafe_rand(); + 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); +} + +#[test(should_fail_with="attempt to add with overflow")] +unconstrained fn mint_private_failure_overflow_recipient() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _) = utils::setup(/* with_account_contracts */ false); + let mint_amount = 10000; + // Mint some tokens + let secret = unsafe_rand(); + 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); + + utils::check_private_balance(token_contract_address, owner, mint_amount); + + let mint_amount = 2.pow_32(128) - mint_amount; + // Mint some tokens + let secret = unsafe_rand(); + 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); +} + +#[test(should_fail_with="attempt to add with overflow")] +unconstrained fn mint_private_failure_overflow_total_supply() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient) = utils::setup(/* with_account_contracts */ false); + let mint_amount = 10000; + // Mint some tokens + let secret_owner = unsafe_rand(); + let secret_recipient = unsafe_rand(); + let secret_hash_owner = compute_secret_hash(secret_owner); + let secret_hash_recipient = compute_secret_hash(secret_recipient); + + let mint_private_call_interface = Token::at(token_contract_address).mint_private(mint_amount, secret_hash_owner); + env.call_public(mint_private_call_interface); + let mint_private_call_interface = Token::at(token_contract_address).mint_private(mint_amount, secret_hash_recipient); + env.call_public(mint_private_call_interface); + + // Time travel so we can read keys from the registry + env.advance_block_by(6); + + // Store 2 notes in the cache so we can redeem it for owner and recipient + env.store_note_in_cache( + &mut TransparentNote::new(mint_amount, secret_hash_owner), + Token::storage().pending_shields.slot, + token_contract_address + ); + env.store_note_in_cache( + &mut TransparentNote::new(mint_amount, secret_hash_recipient), + Token::storage().pending_shields.slot, + token_contract_address + ); + + // Redeem owner's shielded tokens + cheatcodes::set_contract_address(owner); + let redeem_shield_call_interface = Token::at(token_contract_address).redeem_shield(owner, mint_amount, secret_owner); + env.call_private_void(redeem_shield_call_interface); + + // Redeem recipient's shielded tokens + cheatcodes::set_contract_address(recipient); + let redeem_shield_call_interface = Token::at(token_contract_address).redeem_shield(recipient, mint_amount, secret_recipient); + env.call_private_void(redeem_shield_call_interface); + + utils::check_private_balance(token_contract_address, owner, mint_amount); + utils::check_private_balance(token_contract_address, recipient, mint_amount); + + cheatcodes::set_contract_address(owner); + let mint_amount = 2.pow_32(128) - 2 * mint_amount; + // Try to mint some tokens + let secret = unsafe_rand(); + 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); +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/reading_constants.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/reading_constants.nr new file mode 100644 index 00000000000..469ff747590 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/reading_constants.nr @@ -0,0 +1,29 @@ +use crate::test::utils; +use dep::aztec::test::helpers::cheatcodes; +use crate::Token; + +// It is not possible to deserialize strings in Noir ATM, so name and symbol cannot be checked yet. + +#[test] +unconstrained fn check_decimals_private() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, _, _) = utils::setup(/* with_account_contracts */ false); + + // Check decimals + let private_get_decimals_call_interface = Token::at(token_contract_address).private_get_decimals(); + let result = env.call_private(private_get_decimals_call_interface); + + assert(result == 18); +} + +#[test] +unconstrained fn check_decimals_public() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, _, _) = utils::setup(/* with_account_contracts */ false); + + // Check decimals + let public_get_decimals_call_interface = Token::at(token_contract_address).public_get_decimals(); + let result = env.call_public(public_get_decimals_call_interface); + + assert(result == 18 as u8); +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/shielding.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/shielding.nr new file mode 100644 index 00000000000..66280304481 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/shielding.nr @@ -0,0 +1,156 @@ +use crate::test::utils; +use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, hash::compute_secret_hash}; +use dep::authwit::cheatcodes as authwit_cheatcodes; +use crate::{types::transparent_note::TransparentNote, Token}; + +#[test] +unconstrained fn shielding_on_behalf_of_self() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + // Shield tokens + let shield_amount = mint_amount / 10; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, 0); + env.call_public(shield_call_interface); + + // Store a note in the cache so we can redeem it + env.store_note_in_cache( + &mut TransparentNote::new(shield_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, shield_amount, secret); + env.call_private_void(redeem_shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount - shield_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount + shield_amount); +} + +#[test] +unconstrained fn shielding_on_behalf_of_other() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + + // Shield tokens on behalf of owner + let shield_amount = 1000; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, 0); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, recipient, shield_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Shield tokens + env.call_public(shield_call_interface); + + // Become owner again + cheatcodes::set_contract_address(owner); + // Store a note in the cache so we can redeem it + env.store_note_in_cache( + &mut TransparentNote::new(shield_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, shield_amount, secret); + env.call_private_void(redeem_shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount - shield_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount + shield_amount); +} + +#[test] +unconstrained fn shielding_failure_on_behalf_of_self_more_than_balance() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + // Shield tokens + let shield_amount = mint_amount + 1; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, 0); + env.assert_public_call_fails(shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn shielding_failure_on_behalf_of_self_invalid_nonce() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + // Shield tokens + let shield_amount = mint_amount / 10; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, unsafe_rand()); + env.assert_public_call_fails(shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn shielding_failure_on_behalf_of_other_more_than_balance() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + // Shield tokens on behalf of owner + let shield_amount = mint_amount + 1; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, 0); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, recipient, shield_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Shield tokens + env.assert_public_call_fails(shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn shielding_failure_on_behalf_of_other_wrong_caller() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + // Shield tokens on behalf of owner + let shield_amount = mint_amount + 1; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, 0); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, owner, shield_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Shield tokens + env.assert_public_call_fails(shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn shielding_failure_on_behalf_of_other_without_approval() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let secret = unsafe_rand(); + let secret_hash = compute_secret_hash(secret); + // Shield tokens on behalf of owner + let shield_amount = mint_amount + 1; + let shield_call_interface = Token::at(token_contract_address).shield(owner, shield_amount, secret_hash, 0); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Shield tokens + env.assert_public_call_fails(shield_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr new file mode 100644 index 00000000000..47e04809114 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_private.nr @@ -0,0 +1,131 @@ +use crate::test::utils; +use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand, protocol_types::address::AztecAddress}; +use dep::authwit::cheatcodes as authwit_cheatcodes; +use crate::Token; + +#[test] +unconstrained fn transfer_private() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + // Transfer tokens + let transfer_amount = 1000; + let transfer_private_call_interface = Token::at(token_contract_address).transfer(recipient, transfer_amount); + env.call_private_void(transfer_private_call_interface); + + // Check balances + utils::check_private_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_private_balance(token_contract_address, recipient, transfer_amount); +} + +#[test] +unconstrained fn transfer_private_to_self() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + // Transfer tokens + let transfer_amount = 1000; + let transfer_private_call_interface = Token::at(token_contract_address).transfer(owner, transfer_amount); + env.call_private_void(transfer_private_call_interface); + + // Check balances + utils::check_private_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn transfer_private_to_non_deployed_account() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + let not_deployed = cheatcodes::create_account(); + // Transfer tokens + let transfer_amount = 1000; + let transfer_private_call_interface = Token::at(token_contract_address).transfer(not_deployed.address, transfer_amount); + env.call_private_void(transfer_private_call_interface); + + // Check balances + utils::check_private_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_private_balance(token_contract_address, not_deployed.address, transfer_amount); +} + +#[test] +unconstrained fn transfer_private_on_behalf_of_other() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + // Add authwit + let transfer_amount = 1000; + let transfer_private_from_call_interface = Token::at(token_contract_address).transfer_from(owner, recipient, transfer_amount, 1); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, transfer_private_from_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Transfer tokens + env.call_private_void(transfer_private_from_call_interface); + // Check balances + utils::check_private_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_private_balance(token_contract_address, recipient, transfer_amount); +} + +#[test(should_fail_with="Balance too low")] +unconstrained fn transfer_private_failure_more_than_balance() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, _, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + // Transfer tokens + let transfer_amount = mint_amount + 1; + let transfer_private_call_interface = Token::at(token_contract_address).transfer(recipient, transfer_amount); + env.call_private_void(transfer_private_call_interface); +} + +#[test(should_fail_with="invalid nonce")] +unconstrained fn transfer_private_failure_on_behalf_of_self_non_zero_nonce() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, _) = utils::setup_and_mint(/* with_account_contracts */ true); + // Add authwit + let transfer_amount = 1000; + let transfer_private_from_call_interface = Token::at(token_contract_address).transfer_from(owner, recipient, transfer_amount, 1); + // Transfer tokens + env.call_private_void(transfer_private_from_call_interface); +} + +#[test(should_fail_with="Balance too low")] +unconstrained fn transfer_private_failure_on_behalf_of_more_than_balance() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + // Add authwit + let transfer_amount = mint_amount + 1; + let transfer_private_from_call_interface = Token::at(token_contract_address).transfer_from(owner, recipient, transfer_amount, 1); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, transfer_private_from_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Transfer tokens + env.call_private_void(transfer_private_from_call_interface); +} + +#[test(should_fail)] +unconstrained fn transfer_private_failure_on_behalf_of_other_without_approval() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + // Add authwit + let transfer_amount = 1000; + let transfer_private_from_call_interface = Token::at(token_contract_address).transfer_from(owner, recipient, transfer_amount, 1); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Transfer tokens + env.call_private_void(transfer_private_from_call_interface); + // Check balances + utils::check_private_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_private_balance(token_contract_address, recipient, transfer_amount); +} + +#[test(should_fail)] +unconstrained fn transfer_private_failure_on_behalf_of_other_wrong_caller() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + // Add authwit + let transfer_amount = 1000; + let transfer_private_from_call_interface = Token::at(token_contract_address).transfer_from(owner, recipient, transfer_amount, 1); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, owner, transfer_private_from_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Transfer tokens + env.call_private_void(transfer_private_from_call_interface); + // Check balances + utils::check_private_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_private_balance(token_contract_address, recipient, transfer_amount); +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_public.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_public.nr new file mode 100644 index 00000000000..ae0b631ce37 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/transfer_public.nr @@ -0,0 +1,122 @@ +use crate::test::utils; +use dep::aztec::{test::helpers::cheatcodes, oracle::unsafe_rand::unsafe_rand}; +use dep::authwit::cheatcodes as authwit_cheatcodes; +use crate::Token; + +#[test] +unconstrained fn public_transfer() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + // Transfer tokens + let transfer_amount = mint_amount / 10; + let public_transfer_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, 0); + env.call_public(public_transfer_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_public_balance(token_contract_address, recipient, transfer_amount); +} + +#[test] +unconstrained fn public_transfer_to_self() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + // Transfer tokens + let transfer_amount = mint_amount / 10; + let public_transfer_call_interface = Token::at(token_contract_address).transfer_public(owner, owner, transfer_amount, 0); + env.call_public(public_transfer_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn public_transfer_on_behalf_of_other() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let transfer_amount = mint_amount / 10; + let public_transfer_from_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, 1); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, recipient, public_transfer_from_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Transfer tokens + env.call_public(public_transfer_from_call_interface); + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount - transfer_amount); + utils::check_public_balance(token_contract_address, recipient, transfer_amount); +} + +#[test] +unconstrained fn public_transfer_failure_more_than_balance() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + // Transfer tokens + let transfer_amount = mint_amount + 1; + let public_transfer_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, 0); + // Try to transfer tokens + env.assert_public_call_fails(public_transfer_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn public_transfer_failure_on_behalf_of_self_non_zero_nonce() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + // Transfer tokens + let transfer_amount = mint_amount / 10; + let public_transfer_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, unsafe_rand()); + // Try to transfer tokens + env.assert_public_call_fails(public_transfer_call_interface); + + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); +} + +#[test] +unconstrained fn public_transfer_failure_on_behalf_of_other_without_approval() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let transfer_amount = mint_amount / 10; + let public_transfer_from_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, 1); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Try to transfer tokens + env.assert_public_call_fails(public_transfer_from_call_interface); + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_public_balance(token_contract_address, recipient, 0); +} + +#[test] +unconstrained fn public_transfer_failure_on_behalf_of_other_more_than_balance() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let transfer_amount = mint_amount + 1; + let public_transfer_from_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, 1); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, recipient, public_transfer_from_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Try to transfer tokens + env.assert_public_call_fails(public_transfer_from_call_interface); + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_public_balance(token_contract_address, recipient, 0); +} + +#[test] +unconstrained fn public_transfer_failure_on_behalf_of_other_wrong_caller() { + // Setup with account contracts. Slower since we actually deploy them, but needed for authwits. + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + let transfer_amount = mint_amount / 10; + let public_transfer_from_call_interface = Token::at(token_contract_address).transfer_public(owner, recipient, transfer_amount, 1); + authwit_cheatcodes::add_public_authwit_from_call_interface(owner, owner, public_transfer_from_call_interface); + // Impersonate recipient to perform the call + cheatcodes::set_contract_address(recipient); + // Try to transfer tokens + env.assert_public_call_fails(public_transfer_from_call_interface); + // Check balances + utils::check_public_balance(token_contract_address, owner, mint_amount); + utils::check_public_balance(token_contract_address, recipient, 0); +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/unshielding.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/unshielding.nr new file mode 100644 index 00000000000..52987cb1736 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/unshielding.nr @@ -0,0 +1,89 @@ +use crate::test::utils; +use dep::aztec::{oracle::unsafe_rand::unsafe_rand, test::helpers::cheatcodes}; +use dep::authwit::cheatcodes as authwit_cheatcodes; +use crate::Token; + +#[test] +unconstrained fn unshield_on_behalf_of_self() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + let unshield_amount = mint_amount / 10; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, owner, unshield_amount, 0); + env.call_private_void(unshield_call_interface); + utils::check_private_balance(token_contract_address, owner, mint_amount - unshield_amount); + utils::check_public_balance(token_contract_address, owner, mint_amount + unshield_amount); +} + +#[test] +unconstrained fn unshield_on_behalf_of_other() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + let unshield_amount = mint_amount / 10; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, recipient, unshield_amount, 0); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, unshield_call_interface); + // Impersonate recipient + cheatcodes::set_contract_address(recipient); + // Unshield tokens + env.call_private_void(unshield_call_interface); + utils::check_private_balance(token_contract_address, owner, mint_amount - unshield_amount); + utils::check_public_balance(token_contract_address, recipient, unshield_amount); +} + +#[test(should_fail_with="Balance too low")] +unconstrained fn unshield_failure_more_than_balance() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + let unshield_amount = mint_amount + 1; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, owner, unshield_amount, 0); + env.call_private_void(unshield_call_interface); +} + +#[test(should_fail_with="invalid nonce")] +unconstrained fn unshield_failure_on_behalf_of_self_non_zero_nonce() { + // Setup without account contracts. We are not using authwits here, so dummy accounts are enough + let (env, token_contract_address, owner, _, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ false); + + let unshield_amount = mint_amount + 1; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, owner, unshield_amount, unsafe_rand()); + env.call_private_void(unshield_call_interface); +} + +#[test(should_fail_with="Balance too low")] +unconstrained fn unshield_failure_on_behalf_of_other_more_than_balance() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + let unshield_amount = mint_amount + 1; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, recipient, unshield_amount, 0); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, recipient, unshield_call_interface); + // Impersonate recipient + cheatcodes::set_contract_address(recipient); + // Unshield tokens + env.call_private_void(unshield_call_interface); +} + +#[test(should_fail)] +unconstrained fn unshield_failure_on_behalf_of_other_invalid_designated_caller() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + let unshield_amount = mint_amount + 1; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, recipient, unshield_amount, 0); + authwit_cheatcodes::add_private_authwit_from_call_interface(owner, owner, unshield_call_interface); + // Impersonate recipient + cheatcodes::set_contract_address(recipient); + // Unshield tokens + env.call_private_void(unshield_call_interface); +} + +#[test(should_fail)] +unconstrained fn unshield_failure_on_behalf_of_other_no_approval() { + let (env, token_contract_address, owner, recipient, mint_amount) = utils::setup_and_mint(/* with_account_contracts */ true); + + let unshield_amount = mint_amount + 1; + let unshield_call_interface = Token::at(token_contract_address).unshield(owner, recipient, unshield_amount, 0); + // Impersonate recipient + cheatcodes::set_contract_address(recipient); + // Unshield tokens + env.call_private_void(unshield_call_interface); +} diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr new file mode 100644 index 00000000000..1801ddd7213 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/token_contract/src/test/utils.nr @@ -0,0 +1,89 @@ +use dep::aztec::{ + hash::compute_secret_hash, prelude::AztecAddress, + test::helpers::{cheatcodes, test_environment::TestEnvironment}, + protocol_types::storage::map::derive_storage_slot_in_map, + note::{note_getter::{MAX_NOTES_PER_PAGE, view_notes}, note_viewer_options::NoteViewerOptions}, + oracle::{unsafe_rand::unsafe_rand, storage::storage_read} +}; + +use crate::{types::{token_note::TokenNote, transparent_note::TransparentNote}, Token}; + +pub fn setup(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress) { + // Setup env, generate keys + let mut env = TestEnvironment::new(); + let (owner, recipient) = if with_account_contracts { + let owner = env.create_account_contract(1); + let recipient = env.create_account_contract(2); + // Deploy canonical auth registry + let _auth_registry = env.deploy("@aztec/noir-contracts.js/AuthRegistry").without_initializer(); + (owner, recipient) + } else { + let owner = env.create_account(); + let recipient = env.create_account(); + (owner, recipient) + }; + + // 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 = env.deploy("@aztec/noir-contracts.js/Token").with_public_initializer(initializer_call_interface); + let token_contract_address = token_contract.to_address(); + env.advance_block_by(1); + (&mut env, token_contract_address, owner, recipient) +} + +pub fn setup_and_mint(with_account_contracts: bool) -> (&mut TestEnvironment, AztecAddress, AztecAddress, AztecAddress, Field) { + // Setup + let (env, token_contract_address, owner, recipient) = setup(with_account_contracts); + let mint_amount = 10000; + // Mint some tokens + let secret = unsafe_rand(); + 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); + + let mint_public_call_interface = Token::at(token_contract_address).mint_public(owner, mint_amount); + env.call_public(mint_public_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); + + (env, token_contract_address, owner, recipient, mint_amount) +} + +pub fn check_public_balance(token_contract_address: AztecAddress, address: AztecAddress, address_amount: Field) { + let current_contract_address = cheatcodes::get_contract_address(); + cheatcodes::set_contract_address(token_contract_address); + + let balances_slot = Token::storage().public_balances.slot; + let address_slot = derive_storage_slot_in_map(balances_slot, address); + let fields = storage_read(address_slot); + assert(U128::deserialize(fields).to_field() == address_amount, "Public balance is not correct"); + cheatcodes::set_contract_address(current_contract_address); +} + +pub fn check_private_balance(token_contract_address: AztecAddress, address: AztecAddress, address_amount: Field) { + let current_contract_address = cheatcodes::get_contract_address(); + cheatcodes::set_contract_address(token_contract_address); + let balance_of_private = Token::balance_of_private(address); + assert(balance_of_private == address_amount, "Private balance is not correct"); + cheatcodes::set_contract_address(current_contract_address); +} diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/utils.nr b/noir-projects/noir-protocol-circuits/crates/types/src/utils.nr index 95561df1094..88624e25476 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/utils.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/utils.nr @@ -13,7 +13,8 @@ pub fn conditional_assign(predicate: bool, lhs: Field, rhs: Field) -> Field { } pub fn arr_copy_slice(src: [T; N], mut dst: [T; M], offset: u32) -> [T; M] { - for i in 0..dst.len() { + let iterator_len = if N > M { M } else { N }; + for i in 0..iterator_len { dst[i] = src[i + offset]; } dst 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 1875ab0b252..8b763dfcc57 100644 --- a/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs +++ b/noir/noir-repo/aztec_macros/src/transforms/contract_interface.rs @@ -155,9 +155,17 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call name: \"{}\", args_hash, args: args_acc, - original: {} + original: {}, + is_static: {} }}", - args_hash, fn_selector, aztec_visibility, is_static, is_void, fn_name, original + args_hash, + fn_selector, + aztec_visibility, + is_static, + is_void, + fn_name, + original, + is_static_call ) } else { let args = format!( @@ -175,9 +183,17 @@ pub fn stub_function(aztec_visibility: &str, func: &NoirFunction, is_static_call name: \"{}\", args: args_acc, gas_opts: dep::aztec::context::gas::GasOpts::default(), - original: {} + original: {}, + is_static: {} }}", - args, fn_selector, aztec_visibility, is_static, is_void, fn_name, original + args, + fn_selector, + aztec_visibility, + is_static, + is_void, + fn_name, + original, + is_static_call ) }; diff --git a/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/import.rs b/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/import.rs index 343113836ed..9a0be775c30 100644 --- a/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/import.rs +++ b/noir/noir-repo/compiler/noirc_frontend/src/hir/resolution/import.rs @@ -88,15 +88,12 @@ pub fn resolve_import( import_directive: &ImportDirective, def_maps: &BTreeMap, ) -> Result { - let allow_contracts = - allow_referencing_contracts(def_maps, crate_id, import_directive.module_id); - let module_scope = import_directive.module_id; let NamespaceResolution { module_id: resolved_module, namespace: resolved_namespace, mut error, - } = resolve_path_to_ns(import_directive, crate_id, crate_id, def_maps, allow_contracts)?; + } = resolve_path_to_ns(import_directive, crate_id, crate_id, def_maps)?; let name = resolve_path_name(import_directive); @@ -129,20 +126,11 @@ pub fn resolve_import( }) } -fn allow_referencing_contracts( - def_maps: &BTreeMap, - krate: CrateId, - local_id: LocalModuleId, -) -> bool { - ModuleId { krate, local_id }.module(def_maps).is_contract -} - fn resolve_path_to_ns( import_directive: &ImportDirective, crate_id: CrateId, importing_crate: CrateId, def_maps: &BTreeMap, - allow_contracts: bool, ) -> NamespaceResolutionResult { let import_path = &import_directive.path.segments; let def_map = &def_maps[&crate_id]; @@ -150,21 +138,11 @@ fn resolve_path_to_ns( match import_directive.path.kind { crate::ast::PathKind::Crate => { // Resolve from the root of the crate - resolve_path_from_crate_root( - crate_id, - importing_crate, - import_path, - def_maps, - allow_contracts, - ) + resolve_path_from_crate_root(crate_id, importing_crate, import_path, def_maps) + } + crate::ast::PathKind::Dep => { + resolve_external_dep(def_map, import_directive, def_maps, importing_crate) } - crate::ast::PathKind::Dep => resolve_external_dep( - def_map, - import_directive, - def_maps, - allow_contracts, - importing_crate, - ), crate::ast::PathKind::Plain => { // Plain paths are only used to import children modules. It's possible to allow import of external deps, but maybe this distinction is better? // In Rust they can also point to external Dependencies, if no children can be found with the specified name @@ -174,7 +152,6 @@ fn resolve_path_to_ns( import_path, import_directive.module_id, def_maps, - allow_contracts, ) } } @@ -186,7 +163,6 @@ fn resolve_path_from_crate_root( import_path: &[Ident], def_maps: &BTreeMap, - allow_contracts: bool, ) -> NamespaceResolutionResult { resolve_name_in_module( crate_id, @@ -194,7 +170,6 @@ fn resolve_path_from_crate_root( import_path, def_maps[&crate_id].root, def_maps, - allow_contracts, ) } @@ -204,7 +179,6 @@ fn resolve_name_in_module( import_path: &[Ident], starting_mod: LocalModuleId, def_maps: &BTreeMap, - allow_contracts: bool, ) -> NamespaceResolutionResult { let def_map = &def_maps[&krate]; let mut current_mod_id = ModuleId { krate, local_id: starting_mod }; @@ -267,10 +241,6 @@ fn resolve_name_in_module( return Err(PathResolutionError::Unresolved(current_segment.clone())); } - // Check if it is a contract and we're calling from a non-contract context - if current_mod.is_contract && !allow_contracts { - return Err(PathResolutionError::ExternalContractUsed(current_segment.clone())); - } current_ns = found_ns; } @@ -288,7 +258,6 @@ fn resolve_external_dep( current_def_map: &CrateDefMap, directive: &ImportDirective, def_maps: &BTreeMap, - allow_contracts: bool, importing_crate: CrateId, ) -> NamespaceResolutionResult { // Use extern_prelude to get the dep @@ -316,7 +285,7 @@ fn resolve_external_dep( is_prelude: false, }; - resolve_path_to_ns(&dep_directive, dep_module.krate, importing_crate, def_maps, allow_contracts) + resolve_path_to_ns(&dep_directive, dep_module.krate, importing_crate, def_maps) } // Issue an error if the given private function is being called from a non-child module, or diff --git a/yarn-project/pxe/src/index.ts b/yarn-project/pxe/src/index.ts index 7c62b24d3ff..86b3f1205e7 100644 --- a/yarn-project/pxe/src/index.ts +++ b/yarn-project/pxe/src/index.ts @@ -11,3 +11,4 @@ export * from '@aztec/foundation/aztec-address'; export * from '@aztec/key-store'; export * from './database/index.js'; export { ContractDataOracle } from './contract_data_oracle/index.js'; +export { PrivateFunctionsTree } from './contract_data_oracle/private_functions_tree.js'; diff --git a/yarn-project/txe/package.json b/yarn-project/txe/package.json index 5658cd3b454..506521672f5 100644 --- a/yarn-project/txe/package.json +++ b/yarn-project/txe/package.json @@ -18,7 +18,8 @@ "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" + "dev": "DEBUG='aztec:*' && node ./dest/bin/index.js", + "start": "node ./dest/bin/index.js" }, "inherits": [ "../package.common.json" diff --git a/yarn-project/txe/src/bin/index.ts b/yarn-project/txe/src/bin/index.ts index 14934762159..f46cd541c95 100644 --- a/yarn-project/txe/src/bin/index.ts +++ b/yarn-project/txe/src/bin/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env -S node --no-warnings +import { Fr } from '@aztec/foundation/fields'; import { JsonRpcServer } from '@aztec/foundation/json-rpc/server'; import { type Logger, createDebugLogger } from '@aztec/foundation/log'; @@ -32,25 +33,20 @@ class TXEDispatcher { function: functionName, inputs, }: TXEForeignCallInput): Promise { - this.logger.debug( - `Calling ${functionName} with inputs: ${JSON.stringify(inputs, null, 2)} on session ${sessionId}`, - ); + this.logger.debug(`Calling ${functionName} on session ${sessionId}`); if (!TXESessions.has(sessionId) && functionName != 'reset') { - this.logger.debug(`Creating new session ${sessionId}`); + this.logger.info(`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`); + this.logger.info(`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; } } @@ -63,10 +59,11 @@ class TXEDispatcher { * @returns A running http server. */ export function startTXEHttpServer(dispatcher: TXEDispatcher, port: string | number): http.Server { - const txeServer = new JsonRpcServer(dispatcher, {}, {}, ['init']); + const txeServer = new JsonRpcServer(dispatcher, { Fr }, {}, ['init']); const app = txeServer.getApp(); const httpServer = http.createServer(app.callback()); + httpServer.timeout = 1e3 * 60 * 5; // 5 minutes httpServer.listen(port); return httpServer; diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index d3139ee1007..bdf5273f232 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -1,4 +1,5 @@ import { + AuthWitness, L1NotePayload, MerkleTreeId, Note, @@ -11,9 +12,11 @@ import { } from '@aztec/circuit-types'; import { type CircuitWitnessGenerationStats } from '@aztec/circuit-types/stats'; import { - type CompleteAddress, + CallContext, FunctionData, - type Header, + Gas, + GlobalVariables, + Header, type KeyValidationRequest, NULLIFIER_SUBTREE_HEIGHT, type NULLIFIER_TREE_HEIGHT, @@ -23,14 +26,15 @@ import { PrivateCallStackItem, PrivateCircuitPublicInputs, PrivateContextInputs, - type PublicCallRequest, + PublicCallRequest, PublicDataTreeLeaf, type PublicDataTreeLeafPreimage, + TxContext, computeContractClassId, deriveKeys, getContractClassFromArtifact, } from '@aztec/circuits.js'; -import { Aes128 } from '@aztec/circuits.js/barretenberg'; +import { Aes128, Schnorr } 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'; @@ -40,13 +44,16 @@ import { Timer } from '@aztec/foundation/timer'; import { type KeyStore } from '@aztec/key-store'; import { ContractDataOracle } from '@aztec/pxe'; import { + ContractsDataSourcePublicDB, ExecutionError, type ExecutionNoteCache, type MessageLoadOracleInputs, type NoteData, Oracle, type PackedValuesCache, + PublicExecutor, type TypedOracle, + WorldStateDB, acvm, createSimulationError, extractCallStack, @@ -58,12 +65,15 @@ import { type ContractInstance, type ContractInstanceWithAddress } from '@aztec/ import { MerkleTreeSnapshotOperationsFacade, type MerkleTrees } from '@aztec/world-state'; import { type TXEDatabase } from '../util/txe_database.js'; +import { TXEPublicContractDataSource } from '../util/txe_public_contract_data_source.js'; +import { TXEPublicStateDB } from '../util/txe_public_state_db.js'; export class TXE implements TypedOracle { private blockNumber = 0; private sideEffectsCounter = 0; private contractAddress: AztecAddress; private msgSender: AztecAddress; + private functionSelector = FunctionSelector.fromField(new Fr(0)); private contractDataOracle: ContractDataOracle; @@ -85,22 +95,14 @@ export class TXE implements TypedOracle { // Utils - getChainId(): Promise { + getChainId() { return Promise.resolve(this.chainId); } - getVersion(): Promise { + getVersion() { return Promise.resolve(this.version); } - setChainId(chainId: Fr) { - this.chainId = chainId; - } - - setVersion(version: Fr) { - this.version = version; - } - getMsgSender() { return this.msgSender; } @@ -109,6 +111,10 @@ export class TXE implements TypedOracle { this.msgSender = msgSender; } + setFunctionSelector(functionSelector: FunctionSelector) { + this.functionSelector = functionSelector; + } + getSideEffectsCounter() { return this.sideEffectsCounter; } @@ -129,6 +135,10 @@ export class TXE implements TypedOracle { return this.trees; } + getContractDataOracle() { + return this.contractDataOracle; + } + getTXEDatabase() { return this.txeDatabase; } @@ -146,16 +156,24 @@ export class TXE implements TypedOracle { await this.txeDatabase.addContractArtifact(computeContractClassId(contractClass), artifact); } - async getPrivateContextInputs(blockNumber: number, sideEffectsCounter = this.sideEffectsCounter) { + async getPrivateContextInputs( + blockNumber: number, + sideEffectsCounter = this.sideEffectsCounter, + isStaticCall = false, + isDelegateCall = false, + ) { const trees = this.getTrees(); - const stateReference = await trees.getStateReference(true); + const stateReference = await trees.getStateReference(false); 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.callContext.isStaticCall = isStaticCall; + inputs.callContext.isDelegateCall = isDelegateCall; inputs.startSideEffectCounter = sideEffectsCounter; + inputs.callContext.functionSelector = this.functionSelector; return inputs; } @@ -196,13 +214,35 @@ export class TXE implements TypedOracle { return deriveKeys(secret); } + async addAuthWitness(address: AztecAddress, messageHash: Fr) { + const account = this.txeDatabase.getAccount(address); + const privateKey = await this.keyStore.getMasterSecretKey(account.publicKeys.masterIncomingViewingPublicKey); + const schnorr = new Schnorr(); + const signature = schnorr.constructSignature(messageHash.toBuffer(), privateKey).toBuffer(); + const authWitness = new AuthWitness(messageHash, [...signature]); + return this.txeDatabase.addAuthWitness(authWitness.requestHash, authWitness.witness); + } + + async addNullifiers(contractAddress: AztecAddress, nullifiers: Fr[]) { + const db = this.trees.asLatest(); + const siloedNullifiers = nullifiers.map(nullifier => siloNullifier(contractAddress, nullifier).toBuffer()); + + await db.batchInsert(MerkleTreeId.NULLIFIER_TREE, siloedNullifiers, NULLIFIER_SUBTREE_HEIGHT); + } + + async addNoteHashes(contractAddress: AztecAddress, innerNoteHashes: Fr[]) { + const db = this.trees.asLatest(); + const siloedNoteHashes = innerNoteHashes.map(innerNoteHash => siloNoteHash(contractAddress, innerNoteHash)); + await db.appendLeaves(MerkleTreeId.NOTE_HASH_TREE, siloedNoteHashes); + } + // TypedOracle - getBlockNumber(): Promise { + getBlockNumber() { return Promise.resolve(this.blockNumber); } - getContractAddress(): Promise { + getContractAddress() { return Promise.resolve(this.contractAddress); } @@ -210,15 +250,15 @@ export class TXE implements TypedOracle { return Fr.random(); } - packArgumentsArray(args: Fr[]): Promise { + packArgumentsArray(args: Fr[]) { return Promise.resolve(this.packedValuesCache.pack(args)); } - packReturns(returns: Fr[]): Promise { + packReturns(returns: Fr[]) { return Promise.resolve(this.packedValuesCache.pack(returns)); } - unpackReturns(returnsHash: Fr): Promise { + unpackReturns(returnsHash: Fr) { return Promise.resolve(this.packedValuesCache.unpack(returnsHash)); } @@ -227,11 +267,11 @@ export class TXE implements TypedOracle { } async getContractInstance(address: AztecAddress): Promise { - const contractInstance = await this.txeDatabase.getContractInstance(address); + const contractInstance = await this.contractDataOracle.getContractInstance(address); if (!contractInstance) { throw new Error(`Contract instance not found for address ${address}`); } - return Promise.resolve(contractInstance); + return contractInstance; } getMembershipWitness(_blockNumber: number, _treeId: MerkleTreeId, _leafValue: Fr): Promise { @@ -298,12 +338,12 @@ export class TXE implements TypedOracle { throw new Error('Method not implemented.'); } - getCompleteAddress(account: AztecAddress): Promise { + getCompleteAddress(account: AztecAddress) { return Promise.resolve(this.txeDatabase.getAccount(account)); } - getAuthWitness(_messageHash: Fr): Promise { - throw new Error('Method not implemented.'); + getAuthWitness(messageHash: Fr) { + return this.txeDatabase.getAuthWitness(messageHash); } popCapsule(): Promise { @@ -352,7 +392,7 @@ export class TXE implements TypedOracle { return Promise.resolve(notes); } - async notifyCreatedNote(storageSlot: Fr, noteTypeId: Fr, noteItems: Fr[], innerNoteHash: Fr, counter: number) { + notifyCreatedNote(storageSlot: Fr, noteTypeId: Fr, noteItems: Fr[], innerNoteHash: Fr, counter: number) { const note = new Note(noteItems); this.noteCache.addNewNote( { @@ -365,16 +405,11 @@ export class TXE implements TypedOracle { }, counter, ); - const db = this.trees.asLatest(); - const noteHash = siloNoteHash(this.contractAddress, innerNoteHash); - await db.appendLeaves(MerkleTreeId.NOTE_HASH_TREE, [noteHash]); + return Promise.resolve(); } - async notifyNullifiedNote(innerNullifier: Fr, innerNoteHash: Fr, _counter: number) { + 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(); } @@ -461,7 +496,7 @@ export class TXE implements TypedOracle { } emitUnencryptedLog(_log: UnencryptedL2Log, _counter: number): void { - throw new Error('Method not implemented.'); + return; } emitContractClassUnencryptedLog(_log: UnencryptedL2Log, _counter: number): Fr { @@ -473,69 +508,97 @@ export class TXE implements TypedOracle { functionSelector: FunctionSelector, argsHash: Fr, sideEffectCounter: number, - _isStaticCall: boolean, - _isDelegateCall: boolean, + isStaticCall: boolean, + isDelegateCall: boolean, ): Promise { - this.logger.debug( - `Calling private function ${targetContractAddress}:${functionSelector} from ${this.contractAddress}`, + this.logger.verbose( + `Executing external function ${targetContractAddress}:${functionSelector}(${await this.getDebugFunctionName( + targetContractAddress, + functionSelector, + )}) isStaticCall=${isStaticCall} isDelegateCall=${isDelegateCall}`, ); + // Store and modify env const currentContractAddress = AztecAddress.fromField(this.contractAddress); const currentMessageSender = AztecAddress.fromField(this.msgSender); + const currentFunctionSelector = FunctionSelector.fromField(this.functionSelector.toField()); this.setMsgSender(this.contractAddress); this.setContractAddress(targetContractAddress); + this.setFunctionSelector(functionSelector); const artifact = await this.contractDataOracle.getFunctionArtifact(targetContractAddress, functionSelector); const acir = artifact.bytecode; - const initialWitness = await this.getInitialWitness(artifact, argsHash, sideEffectCounter); + const initialWitness = await this.getInitialWitness( + artifact, + argsHash, + sideEffectCounter, + isStaticCall, + isDelegateCall, + ); 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 }, + try { + 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}`); + throw createSimulationError(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, ); - this.logger.debug( - `Error executing private function ${targetContractAddress}:${functionSelector}\n${createSimulationError( - execError, - )}`, + // Apply side effects + this.sideEffectsCounter = publicInputs.endSideEffectCounter.toNumber(); + + await this.addNullifiers( + targetContractAddress, + publicInputs.newNullifiers.filter(nullifier => !nullifier.isEmpty()).map(nullifier => nullifier.value), + ); + + await this.addNoteHashes( + targetContractAddress, + publicInputs.newNoteHashes.filter(noteHash => !noteHash.isEmpty()).map(noteHash => noteHash.value), ); - 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; + return callStackItem; + } finally { + this.setContractAddress(currentContractAddress); + this.setMsgSender(currentMessageSender); + this.setFunctionSelector(currentFunctionSelector); + } } - async getInitialWitness(abi: FunctionAbi, argsHash: Fr, sideEffectCounter: number) { + async getInitialWitness( + abi: FunctionAbi, + argsHash: Fr, + sideEffectCounter: number, + isStaticCall: boolean, + isDelegateCall: boolean, + ) { const argumentsSize = countArgumentsSize(abi); const args = this.packedValuesCache.unpack(argsHash); @@ -544,33 +607,220 @@ export class TXE implements TypedOracle { throw new Error('Invalid arguments size'); } - const privateContextInputs = await this.getPrivateContextInputs(this.blockNumber - 1, sideEffectCounter); + const privateContextInputs = await this.getPrivateContextInputs( + this.blockNumber - 1, + sideEffectCounter, + isStaticCall, + isDelegateCall, + ); const fields = [...privateContextInputs.toFields(), ...args]; return toACVMWitness(0, fields); } - callPublicFunction( - _targetContractAddress: AztecAddress, - _functionSelector: FunctionSelector, - _argsHash: Fr, - _sideEffectCounter: number, - _isStaticCall: boolean, - _isDelegateCall: boolean, + public async getDebugFunctionName(address: AztecAddress, selector: FunctionSelector): Promise { + const instance = await this.contractDataOracle.getContractInstance(address); + if (!instance) { + return undefined; + } + const artifact = await this.contractDataOracle.getContractArtifact(instance!.contractClassId); + if (!artifact) { + return undefined; + } + + const f = artifact.functions.find(f => + FunctionSelector.fromNameAndParameters(f.name, f.parameters).equals(selector), + ); + if (!f) { + return undefined; + } + + return `${artifact.name}:${f.name}`; + } + + async executePublicFunction( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + args: Fr[], + callContext: CallContext, + ) { + const header = Header.empty(); + header.state = await this.trees.getStateReference(true); + header.globalVariables.blockNumber = new Fr(await this.getBlockNumber()); + header.state.partial.nullifierTree.root = Fr.fromBuffer( + (await this.trees.getTreeInfo(MerkleTreeId.NULLIFIER_TREE, true)).root, + ); + header.state.partial.noteHashTree.root = Fr.fromBuffer( + (await this.trees.getTreeInfo(MerkleTreeId.NOTE_HASH_TREE, true)).root, + ); + header.state.partial.publicDataTree.root = Fr.fromBuffer( + (await this.trees.getTreeInfo(MerkleTreeId.PUBLIC_DATA_TREE, true)).root, + ); + header.state.l1ToL2MessageTree.root = Fr.fromBuffer( + (await this.trees.getTreeInfo(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, true)).root, + ); + const executor = new PublicExecutor( + new TXEPublicStateDB(this), + new ContractsDataSourcePublicDB(new TXEPublicContractDataSource(this)), + new WorldStateDB(this.trees.asLatest()), + header, + ); + const execution = { + contractAddress: targetContractAddress, + functionSelector, + args, + callContext, + }; + + return executor.simulate( + execution, + GlobalVariables.empty(), + Gas.test(), + TxContext.empty(), + /* pendingNullifiers */ [], + /* transactionFee */ Fr.ZERO, + callContext.sideEffectCounter, + ); + } + + async avmOpcodeCall( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + args: Fr[], + isStaticCall: boolean, + isDelegateCall: boolean, + ) { + // Store and modify env + const currentContractAddress = AztecAddress.fromField(this.contractAddress); + const currentMessageSender = AztecAddress.fromField(this.msgSender); + const currentFunctionSelector = FunctionSelector.fromField(this.functionSelector.toField()); + this.setMsgSender(this.contractAddress); + this.setContractAddress(targetContractAddress); + this.setFunctionSelector(functionSelector); + + const callContext = CallContext.empty(); + callContext.msgSender = this.msgSender; + callContext.functionSelector = this.functionSelector; + callContext.sideEffectCounter = this.sideEffectsCounter; + callContext.storageContractAddress = targetContractAddress; + callContext.isStaticCall = isStaticCall; + callContext.isDelegateCall = isDelegateCall; + + const executionResult = await this.executePublicFunction( + targetContractAddress, + functionSelector, + args, + callContext, + ); + + // Apply side effects + if (!executionResult.reverted) { + this.sideEffectsCounter += executionResult.endSideEffectCounter.toNumber(); + } + this.setContractAddress(currentContractAddress); + this.setMsgSender(currentMessageSender); + this.setFunctionSelector(currentFunctionSelector); + + return executionResult; + } + + async callPublicFunction( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + argsHash: Fr, + sideEffectCounter: number, + isStaticCall: boolean, + isDelegateCall: boolean, ): Promise { - throw new Error('Method not implemented.'); + // Store and modify env + const currentContractAddress = AztecAddress.fromField(this.contractAddress); + const currentMessageSender = AztecAddress.fromField(this.msgSender); + const currentFunctionSelector = FunctionSelector.fromField(this.functionSelector.toField()); + this.setMsgSender(this.contractAddress); + this.setContractAddress(targetContractAddress); + this.setFunctionSelector(functionSelector); + + const callContext = CallContext.empty(); + callContext.msgSender = this.msgSender; + callContext.functionSelector = this.functionSelector; + callContext.sideEffectCounter = sideEffectCounter; + callContext.storageContractAddress = targetContractAddress; + callContext.isStaticCall = isStaticCall; + callContext.isDelegateCall = isDelegateCall; + + const args = this.packedValuesCache.unpack(argsHash); + + const executionResult = await this.executePublicFunction( + targetContractAddress, + functionSelector, + args, + callContext, + ); + + // Apply side effects + this.sideEffectsCounter = executionResult.endSideEffectCounter.toNumber(); + this.setContractAddress(currentContractAddress); + this.setMsgSender(currentMessageSender); + this.setFunctionSelector(currentFunctionSelector); + + return executionResult.returnValues; } - enqueuePublicFunctionCall( - _targetContractAddress: AztecAddress, - _functionSelector: FunctionSelector, - _argsHash: Fr, - _sideEffectCounter: number, - _isStaticCall: boolean, - _isDelegateCall: boolean, + async enqueuePublicFunctionCall( + targetContractAddress: AztecAddress, + functionSelector: FunctionSelector, + argsHash: Fr, + sideEffectCounter: number, + isStaticCall: boolean, + isDelegateCall: boolean, ): Promise { - throw new Error('Method not implemented.'); + // Store and modify env + const currentContractAddress = AztecAddress.fromField(this.contractAddress); + const currentMessageSender = AztecAddress.fromField(this.msgSender); + const currentFunctionSelector = FunctionSelector.fromField(this.functionSelector.toField()); + this.setMsgSender(this.contractAddress); + this.setContractAddress(targetContractAddress); + this.setFunctionSelector(functionSelector); + + const callContext = CallContext.empty(); + callContext.msgSender = this.msgSender; + callContext.functionSelector = this.functionSelector; + callContext.sideEffectCounter = sideEffectCounter; + callContext.storageContractAddress = targetContractAddress; + callContext.isStaticCall = isStaticCall; + callContext.isDelegateCall = isDelegateCall; + + const args = this.packedValuesCache.unpack(argsHash); + + const executionResult = await this.executePublicFunction( + targetContractAddress, + functionSelector, + args, + callContext, + ); + + // Apply side effects + this.sideEffectsCounter += executionResult.endSideEffectCounter.toNumber(); + this.setContractAddress(currentContractAddress); + this.setMsgSender(currentMessageSender); + this.setFunctionSelector(currentFunctionSelector); + + const parentCallContext = CallContext.empty(); + parentCallContext.msgSender = currentMessageSender; + parentCallContext.functionSelector = currentFunctionSelector; + parentCallContext.sideEffectCounter = sideEffectCounter; + parentCallContext.storageContractAddress = currentContractAddress; + parentCallContext.isStaticCall = isStaticCall; + parentCallContext.isDelegateCall = isDelegateCall; + + return PublicCallRequest.from({ + parentCallContext, + contractAddress: targetContractAddress, + functionSelector, + callContext, + args, + }); } setPublicTeardownFunctionCall( diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index 6b53c0de1eb..aef0d8e3d49 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -13,7 +13,6 @@ 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'; @@ -28,10 +27,11 @@ import { toForeignCallResult, toSingle, } from '../util/encoding.js'; +import { ExpectedFailureError } from '../util/expected_failure_error.js'; import { TXEDatabase } from '../util/txe_database.js'; export class TXEService { - constructor(private logger: Logger, private typedOracle: TypedOracle, private store: AztecKVStore) {} + constructor(private logger: Logger, private typedOracle: TypedOracle) {} static async init(logger: Logger) { const store = openTmpStore(true); @@ -42,8 +42,8 @@ export class TXEService { 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))); + const service = new TXEService(logger, txe); + await service.advanceBlocksBy(toSingle(new Fr(1n))); return service; } @@ -59,31 +59,20 @@ export class TXEService { return toForeignCallResult(inputs.toFields().map(toSingle)); } - async timeTravel(blocks: ForeignCallSingle) { + async advanceBlocksBy(blocks: ForeignCallSingle) { const nBlocks = fromSingle(blocks).toNumber(); - this.logger.info(`time traveling ${nBlocks} blocks`); + this.logger.debug(`time traveling ${nBlocks} blocks`); const trees = (this.typedOracle as TXE).getTrees(); + 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); + l2Block.archive.root = Fr.fromBuffer((await trees.getTreeInfo(MerkleTreeId.ARCHIVE, true)).root); + l2Block.header = header; 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); } @@ -115,7 +104,10 @@ export class TXEService { .map(char => String.fromCharCode(char.toNumber())) .join(''); const decodedArgs = fromArray(args); - this.logger.debug(`Deploy ${pathStr} with ${initializerStr} and ${decodedArgs}`); + const publicKeysHashFr = fromSingle(publicKeysHash); + this.logger.debug( + `Deploy ${pathStr} with initializer ${initializerStr}(${decodedArgs}) and public keys hash ${publicKeysHashFr}`, + ); 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]]; @@ -123,7 +115,7 @@ export class TXEService { constructorArgs: decodedArgs, skipArgsDecoding: true, salt: Fr.ONE, - publicKeysHash: fromSingle(publicKeysHash), + publicKeysHash: publicKeysHashFr, constructorArtifact: initializerStr ? initializerStr : undefined, deployer: AztecAddress.ZERO, }); @@ -131,7 +123,15 @@ export class TXEService { 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)]); + return toForeignCallResult([ + toArray([ + instance.salt, + instance.deployer, + instance.contractClassId, + instance.initializationHash, + instance.publicKeysHash, + ]), + ]); } async directStorageWrite( @@ -175,6 +175,7 @@ export class TXEService { const completeAddress = await keyStore.addAccount(fromSingle(secret), fromSingle(partialAddress)); 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), @@ -196,6 +197,59 @@ export class TXEService { return toForeignCallResult([toSingle(new Fr(counter))]); } + async addAuthWitness(address: ForeignCallSingle, messageHash: ForeignCallSingle) { + await (this.typedOracle as TXE).addAuthWitness(fromSingle(address), fromSingle(messageHash)); + return toForeignCallResult([]); + } + + async assertPublicCallFails( + address: ForeignCallSingle, + functionSelector: ForeignCallSingle, + _length: ForeignCallSingle, + args: ForeignCallArray, + ) { + const parsedAddress = fromSingle(address); + const parsedSelector = FunctionSelector.fromField(fromSingle(functionSelector)); + const result = await (this.typedOracle as TXE).avmOpcodeCall( + parsedAddress, + parsedSelector, + fromArray(args), + false, + false, + ); + if (!result.reverted) { + throw new ExpectedFailureError('Public call did not revert'); + } + + return toForeignCallResult([]); + } + + async assertPrivateCallFails( + targetContractAddress: ForeignCallSingle, + functionSelector: ForeignCallSingle, + argsHash: ForeignCallSingle, + sideEffectCounter: ForeignCallSingle, + isStaticCall: ForeignCallSingle, + isDelegateCall: ForeignCallSingle, + ) { + try { + await this.typedOracle.callPrivateFunction( + fromSingle(targetContractAddress), + FunctionSelector.fromField(fromSingle(functionSelector)), + fromSingle(argsHash), + fromSingle(sideEffectCounter).toNumber(), + fromSingle(isStaticCall).toBool(), + fromSingle(isDelegateCall).toBool(), + ); + throw new ExpectedFailureError('Private call did not fail'); + } catch (e) { + if (e instanceof ExpectedFailureError) { + throw e; + } + } + return toForeignCallResult([]); + } + // PXE oracles getRandomField() { @@ -433,10 +487,27 @@ export class TXEService { return toForeignCallResult([toSingle(new Fr(exists))]); } + async avmOpcodeCall( + _gas: ForeignCallArray, + address: ForeignCallSingle, + _length: ForeignCallSingle, + args: ForeignCallArray, + functionSelector: ForeignCallSingle, + ) { + const result = await (this.typedOracle as TXE).avmOpcodeCall( + fromSingle(address), + FunctionSelector.fromField(fromSingle(functionSelector)), + fromArray(args), + false, + false, + ); + + return toForeignCallResult([toArray(result.returnValues), toSingle(new Fr(1))]); + } + async getPublicKeysAndPartialAddress(address: ForeignCallSingle) { const parsedAddress = AztecAddress.fromField(fromSingle(address)); const { publicKeys, partialAddress } = await this.typedOracle.getCompleteAddress(parsedAddress); - return toForeignCallResult([toArray([...publicKeys.toFields(), partialAddress])]); } @@ -519,4 +590,56 @@ export class TXEService { } return toForeignCallResult([toArray(witness.toFields())]); } + + async getAuthWitness(messageHash: ForeignCallSingle) { + const parsedMessageHash = fromSingle(messageHash); + const authWitness = await this.typedOracle.getAuthWitness(parsedMessageHash); + if (!authWitness) { + throw new Error(`Auth witness not found for message hash ${parsedMessageHash}.`); + } + return toForeignCallResult([toArray(authWitness)]); + } + + async enqueuePublicFunctionCall( + targetContractAddress: ForeignCallSingle, + functionSelector: ForeignCallSingle, + argsHash: ForeignCallSingle, + sideEffectCounter: ForeignCallSingle, + isStaticCall: ForeignCallSingle, + isDelegateCall: ForeignCallSingle, + ) { + const publicCallRequest = await this.typedOracle.enqueuePublicFunctionCall( + fromSingle(targetContractAddress), + FunctionSelector.fromField(fromSingle(functionSelector)), + fromSingle(argsHash), + fromSingle(sideEffectCounter).toNumber(), + fromSingle(isStaticCall).toBool(), + fromSingle(isDelegateCall).toBool(), + ); + const fields = [ + publicCallRequest.contractAddress.toField(), + publicCallRequest.functionSelector.toField(), + ...publicCallRequest.callContext.toFields(), + publicCallRequest.getArgsHash(), + ]; + return toForeignCallResult([toArray(fields)]); + } + + async getChainId() { + return toForeignCallResult([toSingle(await this.typedOracle.getChainId())]); + } + + async getVersion() { + return toForeignCallResult([toSingle(await this.typedOracle.getVersion())]); + } + + async addNullifiers(contractAddress: ForeignCallSingle, _length: ForeignCallSingle, nullifiers: ForeignCallArray) { + await (this.typedOracle as TXE).addNullifiers(fromSingle(contractAddress), fromArray(nullifiers)); + return toForeignCallResult([]); + } + + async addNoteHashes(contractAddress: ForeignCallSingle, _length: ForeignCallSingle, noteHashes: ForeignCallArray) { + await (this.typedOracle as TXE).addNoteHashes(fromSingle(contractAddress), fromArray(noteHashes)); + return toForeignCallResult([]); + } } diff --git a/yarn-project/txe/src/util/expected_failure_error.ts b/yarn-project/txe/src/util/expected_failure_error.ts new file mode 100644 index 00000000000..8f97a3ae2bf --- /dev/null +++ b/yarn-project/txe/src/util/expected_failure_error.ts @@ -0,0 +1,5 @@ +export class ExpectedFailureError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/yarn-project/txe/src/util/txe_public_contract_data_source.ts b/yarn-project/txe/src/util/txe_public_contract_data_source.ts new file mode 100644 index 00000000000..64f410f9595 --- /dev/null +++ b/yarn-project/txe/src/util/txe_public_contract_data_source.ts @@ -0,0 +1,63 @@ +import { type AztecAddress, Fr, type FunctionSelector, unpackBytecode } from '@aztec/circuits.js'; +import { type ContractArtifact } from '@aztec/foundation/abi'; +import { PrivateFunctionsTree } from '@aztec/pxe'; +import { + type ContractClassPublic, + type ContractDataSource, + type ContractInstanceWithAddress, + type PublicFunction, +} from '@aztec/types/contracts'; + +import { type TXE } from '../oracle/txe_oracle.js'; + +export class TXEPublicContractDataSource implements ContractDataSource { + constructor(private txeOracle: TXE) {} + + async getPublicFunction(address: AztecAddress, selector: FunctionSelector): Promise { + const bytecode = await this.txeOracle.getContractDataOracle().getBytecode(address, selector); + if (!bytecode) { + return undefined; + } + return { bytecode, selector }; + } + + getBlockNumber(): Promise { + return this.txeOracle.getBlockNumber(); + } + + async getContractClass(id: Fr): Promise { + const contractClass = await this.txeOracle.getContractDataOracle().getContractClass(id); + const artifact = await this.txeOracle.getContractDataOracle().getContractArtifact(id); + const tree = new PrivateFunctionsTree(artifact); + const privateFunctionsRoot = tree.getFunctionTreeRoot(); + + return { + id, + artifactHash: contractClass!.artifactHash, + packedBytecode: contractClass!.packedBytecode, + publicFunctions: unpackBytecode(contractClass!.packedBytecode), + privateFunctionsRoot: new Fr(privateFunctionsRoot!.root), + version: contractClass!.version, + privateFunctions: [], + unconstrainedFunctions: [], + }; + } + + async getContract(address: AztecAddress): Promise { + const instance = await this.txeOracle.getContractDataOracle().getContractInstance(address); + return { ...instance, address }; + } + + getContractClassIds(): Promise { + throw new Error('Method not implemented.'); + } + + async getContractArtifact(address: AztecAddress): Promise { + const instance = await this.txeOracle.getContractDataOracle().getContractInstance(address); + return this.txeOracle.getContractDataOracle().getContractArtifact(instance.contractClassId); + } + + addContractArtifact(address: AztecAddress, contract: ContractArtifact): Promise { + return this.txeOracle.addContractArtifact(contract); + } +} diff --git a/yarn-project/txe/src/util/txe_public_state_db.ts b/yarn-project/txe/src/util/txe_public_state_db.ts new file mode 100644 index 00000000000..62bdbaf7e5b --- /dev/null +++ b/yarn-project/txe/src/util/txe_public_state_db.ts @@ -0,0 +1,57 @@ +import { MerkleTreeId } from '@aztec/circuit-types'; +import { + type AztecAddress, + Fr, + PUBLIC_DATA_SUBTREE_HEIGHT, + PublicDataTreeLeaf, + type PublicDataTreeLeafPreimage, +} from '@aztec/circuits.js'; +import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; +import { type PublicStateDB } from '@aztec/simulator'; + +import { type TXE } from '../oracle/txe_oracle.js'; + +export class TXEPublicStateDB implements PublicStateDB { + constructor(private txeOracle: TXE) {} + + async storageRead(contract: AztecAddress, slot: Fr): Promise { + const db = this.txeOracle.getTrees().asLatest(); + const leafSlot = computePublicDataTreeLeafSlot(contract, slot).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; + } + return value; + } + + async storageWrite(contract: AztecAddress, slot: Fr, newValue: Fr): Promise { + const db = this.txeOracle.getTrees().asLatest(); + + await db.batchInsert( + MerkleTreeId.PUBLIC_DATA_TREE, + [new PublicDataTreeLeaf(computePublicDataTreeLeafSlot(contract, slot), newValue).toBuffer()], + PUBLIC_DATA_SUBTREE_HEIGHT, + ); + return newValue.toBigInt(); + } + + checkpoint(): Promise { + return Promise.resolve(); + } + rollbackToCheckpoint(): Promise { + throw new Error('Cannot rollback'); + } + commit(): Promise { + return Promise.resolve(); + } + rollbackToCommit(): Promise { + throw new Error('Cannot rollback'); + } +}