diff --git a/docs/docs-developers/docs/aztec-nr/testing_contracts.md b/docs/docs-developers/docs/aztec-nr/testing_contracts.md index 03d19207600d..fbca84ea0536 100644 --- a/docs/docs-developers/docs/aztec-nr/testing_contracts.md +++ b/docs/docs-developers/docs/aztec-nr/testing_contracts.md @@ -307,6 +307,32 @@ env.advance_next_block_timestamp_by(duration); env.mine_block_at(block_timestamp); ```` +## Testing private events [experimental] + +> Testing events API is currently experimental as we don't yet support filtering or pagination and there will be breaking changes. + +You can verify that private events were emitted correctly in the last call using `get_private_events`: + +```rust +#[test] +unconstrained fn test_transfer_emits_event() { + let (env, token_address, owner, recipient) = setup(false); + + env.call_private(owner, Token::at(token_address).transfer(recipient, 100)); + + let events = env.get_private_events::(token_address, recipient); + assert_eq(events.len(), 1); + let event = events.get(0); + assert_eq(event.from, owner); + assert_eq(event.to, recipient); + assert_eq(event.amount, 100 as u128); +} +``` + +:::info +The `#[event]` macro automatically applies `pub` visibility to the struct, making it accessible from the test crate. +::: + ## Testing failure cases Test functions that should fail using annotations: diff --git a/noir-projects/aztec-nr/aztec/src/macros/events.nr b/noir-projects/aztec-nr/aztec/src/macros/events.nr index 62c71634d32c..d6f11c3e0b50 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/events.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/events.nr @@ -1,5 +1,8 @@ use crate::{ - macros::utils::{compute_struct_selector, derive_serialize_if_not_implemented, get_trait_impl_method}, + macros::utils::{ + compute_struct_selector, derive_deserialize_if_not_implemented, derive_serialize_if_not_implemented, + get_trait_impl_method, + }, utils::cmap::CHashMap, }; use std::panic; @@ -61,6 +64,8 @@ comptime fn register_event_selector(event_selector: Field, event_name: Quoted) { /// [`EventInterface`](crate::event::event_interface::EventInterface) implementation (which provides the event type /// id) and a [`Serialize`](crate::protocol::traits::Serialize) implementation if one is not already provided. /// +/// The macro automatically applies `pub` visibility to the struct. +/// /// ## Requirements /// /// The event struct must not exceed @@ -70,11 +75,14 @@ pub comptime fn event(s: TypeDefinition) -> Quoted { register_event_selector(event_selector, s.name()); let serialize_impl = derive_serialize_if_not_implemented(s); + let deserialize_impl = derive_deserialize_if_not_implemented(s); s.add_attribute("abi(events)"); + s.set_visibility(quote { pub }); quote { $event_interface_impl $serialize_impl + $deserialize_impl } } diff --git a/noir-projects/aztec-nr/aztec/src/macros/utils.nr b/noir-projects/aztec-nr/aztec/src/macros/utils.nr index 151079bb208b..a2ee1ccbd225 100644 --- a/noir-projects/aztec-nr/aztec/src/macros/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/macros/utils.nr @@ -1,4 +1,4 @@ -use crate::protocol::meta::derive_serialize; +use crate::protocol::meta::{derive_deserialize, derive_serialize}; use std::meta::{ctstring::AsCtString, unquote}; pub(crate) comptime fn is_fn_external(f: FunctionDefinition) -> bool { @@ -164,3 +164,14 @@ pub comptime fn derive_serialize_if_not_implemented(s: TypeDefinition) -> Quoted derive_serialize(s) } } + +/// Returns an `impl` for `Deserialize` if not already implemented. +/// +/// The returned quote will be empty if the type `s` already implements `Deserialize`. +pub comptime fn derive_deserialize_if_not_implemented(s: TypeDefinition) -> Quoted { + if s.as_type().implements(quote { crate::protocol::traits::Deserialize }.as_trait_constraint()) { + quote {} + } else { + derive_deserialize(s) + } +} 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 c48e8b5c023c..22b150b23c54 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 @@ -838,6 +838,36 @@ impl TestEnvironment { ); } + /// Returns private events emitted as of last block. + /// + /// Queries up to `MAX_PRIVATE_EVENTS_PER_TXE_QUERY` private events of type `Event` that were emitted by + /// `contract_address` and are readable by `scope` (i.e. the address under which the event was added to pxe, this + /// is typically the event recipient). + /// + /// **Experimental** - there will be breaking API changes. + /// + /// --- + /// + /// ## Examples + /// + /// Emitting a single event in a contract call and reading it back (transfer function emits the event): + /// ```noir + /// env.call_private(owner, Token::at(token_address).transfer(recipient, 100)); + /// + /// let events = env.get_private_events::(token_address, recipient); + /// assert_eq(events.get(0).amount, 100 as u128); + /// ``` + pub unconstrained fn get_private_events( + _self: Self, + contract_address: AztecAddress, + scope: AztecAddress, + ) -> BoundedVec + where + Event: EventInterface + Deserialize, + { + txe_oracles::get_private_events(contract_address, scope) + } + unconstrained fn discover_event_opts( self: Self, opts: EventDiscoveryOptions, diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/events.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/events.nr index 9e50aa8d6322..1ef6374ac8ae 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/events.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/events.nr @@ -1,5 +1,4 @@ -use crate::event::{event_emission::emit_event_in_private, event_interface::EventInterface}; -use crate::protocol::traits::Serialize; +use crate::event::event_emission::emit_event_in_private; use crate::test::{helpers::test_environment::TestEnvironment, mocks::mock_event::MockEvent}; global VALUE: Field = 7; @@ -16,14 +15,10 @@ unconstrained fn emit_and_discover_event() { env.discover_event(event_message, recipient); - let events = crate::test::helpers::txe_oracles::get_private_events( - MockEvent::get_event_type_id(), - TestEnvironment::get_default_address(), - recipient, - ); + let events: BoundedVec = env.get_private_events(TestEnvironment::get_default_address(), recipient); assert_eq(events.len(), 1); - assert_eq(events.get(0), BoundedVec::from_array(event.serialize())); + assert_eq(events.get(0), event); } #[test] @@ -42,15 +37,11 @@ unconstrained fn emit_and_discover_multiple_events() { env.discover_event(event_messages.0, recipient); env.discover_event(event_messages.1, recipient); - let events = crate::test::helpers::txe_oracles::get_private_events( - MockEvent::get_event_type_id(), - TestEnvironment::get_default_address(), - recipient, - ); + let events: BoundedVec = env.get_private_events(TestEnvironment::get_default_address(), recipient); assert_eq(events.len(), 2); - assert(events.any(|ev| ev == BoundedVec::from_array(event.serialize()))); - assert(events.any(|ev| ev == BoundedVec::from_array(other_event.serialize()))); + assert(events.any(|ev| ev == event)); + assert(events.any(|ev| ev == other_event)); } #[test] @@ -66,14 +57,10 @@ unconstrained fn emit_and_discover_same_event_multiple_times() { env.discover_event(event_message, recipient); env.discover_event(event_message, recipient); - let events = crate::test::helpers::txe_oracles::get_private_events( - MockEvent::get_event_type_id(), - TestEnvironment::get_default_address(), - recipient, - ); + let events: BoundedVec = env.get_private_events(TestEnvironment::get_default_address(), recipient); assert_eq(events.len(), 1); - assert_eq(events.get(0), BoundedVec::from_array(event.serialize())); + assert_eq(events.get(0), event); } #[test] @@ -89,11 +76,8 @@ unconstrained fn emit_and_fail_to_discover_event_as_other_recipient() { env.discover_event(event_message, recipient); - let events = crate::test::helpers::txe_oracles::get_private_events( - MockEvent::get_event_type_id(), - TestEnvironment::get_default_address(), - other_recipient, - ); + let events: BoundedVec = + env.get_private_events(TestEnvironment::get_default_address(), other_recipient); assert_eq(events.len(), 0); } @@ -112,12 +96,8 @@ unconstrained fn emit_and_discover_event_multiple_recipients() { env.discover_event(event_message, recipient); env.discover_event(event_message, other_recipient); - let events = crate::test::helpers::txe_oracles::get_private_events( - MockEvent::get_event_type_id(), - TestEnvironment::get_default_address(), - recipient, - ); + let events: BoundedVec = env.get_private_events(TestEnvironment::get_default_address(), recipient); assert_eq(events.len(), 1); - assert_eq(events.get(0), BoundedVec::from_array(event.serialize())); + assert_eq(events.get(0), event); } diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr index 608352e06d6b..810d862a7e88 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr @@ -1,4 +1,8 @@ -use crate::{context::inputs::PrivateContextInputs, event::EventSelector, test::helpers::utils::TestAccount}; +use crate::{ + context::inputs::PrivateContextInputs, + event::{event_interface::EventInterface, EventSelector}, + test::helpers::utils::TestAccount, +}; use crate::protocol::{ abis::function_selector::FunctionSelector, @@ -8,9 +12,10 @@ use crate::protocol::{ }, contract_instance::ContractInstance, traits::{Deserialize, ToField}, + utils::reader::Reader, }; -global MAX_PRIVATE_EVENTS_PER_TXE_QUERY: u32 = 5; +pub(crate) global MAX_PRIVATE_EVENTS_PER_TXE_QUERY: u32 = 5; global MAX_EVENT_SERIALIZATION_LENGTH: u32 = 10; pub unconstrained fn deploy( @@ -78,20 +83,27 @@ pub unconstrained fn get_last_tx_effects() -> (Field, BoundedVec AztecAddress {} -// experimental -pub(crate) unconstrained fn get_private_events( - selector: EventSelector, +pub(crate) unconstrained fn get_private_events( contract_address: AztecAddress, scope: AztecAddress, -) -> BoundedVec, MAX_PRIVATE_EVENTS_PER_TXE_QUERY> { +) -> BoundedVec +where + Event: EventInterface + Deserialize, +{ // This is a workaround as Noir does not currently let us return nested structs with arrays. We instead return a // raw multidimensional array in get_private_events_oracle and create the BoundedVecs here. - + let selector = Event::get_event_type_id(); let (raw_array_storage, event_lengths, query_length) = get_private_events_oracle(selector, contract_address, scope); let mut events = BoundedVec::new(); for i in 0..query_length { - events.push(BoundedVec::from_parts(raw_array_storage[i], event_lengths[i])); + assert( + ::N == event_lengths[i], + "Unexpected event length obtained from txeGetPrivateEvents", + ); + + let mut reader = Reader::new(raw_array_storage[i]); + events.push(Deserialize::stream_deserialize(&mut reader)); } events diff --git a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_event.nr b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_event.nr index e8a46946acda..d25b15c26ccb 100644 --- a/noir-projects/aztec-nr/aztec/src/test/mocks/mock_event.nr +++ b/noir-projects/aztec-nr/aztec/src/test/mocks/mock_event.nr @@ -2,9 +2,9 @@ use crate::{event::{event_emission::NewEvent, event_interface::EventInterface, EventSelector}, oracle::random::random}; -use crate::protocol::traits::{FromField, Serialize}; +use crate::protocol::traits::{Deserialize, FromField, Serialize}; -#[derive(Eq, Serialize)] +#[derive(Eq, Serialize, Deserialize)] pub(crate) struct MockEvent { pub(crate) value: Field, } diff --git a/noir-projects/noir-contracts-comp-failures/contracts/invalid_event/src/invalid_event.nr b/noir-projects/noir-contracts-comp-failures/contracts/invalid_event/src/invalid_event.nr index a38ba6855727..e36913eadbdb 100644 --- a/noir-projects/noir-contracts-comp-failures/contracts/invalid_event/src/invalid_event.nr +++ b/noir-projects/noir-contracts-comp-failures/contracts/invalid_event/src/invalid_event.nr @@ -5,7 +5,7 @@ use aztec::{ }; #[event] -pub struct InvalidEvent {} +struct InvalidEvent {} impl Serialize for InvalidEvent { let N: u32 = MAX_EVENT_SERIALIZED_LEN + 1; diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr index c4378f376926..5872c9dd8e8e 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr @@ -46,9 +46,9 @@ pub contract Token { #[event] struct Transfer { - from: AztecAddress, - to: AztecAddress, - amount: u128, + pub from: AztecAddress, + pub to: AztecAddress, + pub amount: u128, } // docs:start:storage_struct @@ -243,7 +243,7 @@ pub contract Token { self.storage.balances.at(from).add(change).deliver(MessageDelivery.ONCHAIN_UNCONSTRAINED); self.storage.balances.at(to).add(amount).deliver(MessageDelivery.ONCHAIN_UNCONSTRAINED); - // We don't constrain encryption of the note log in `transfer` (unlike in `transfer_in_private`) because the transfer + // We don't constrain encryption of the event log here (unlike in `transfer_in_private`) because the transfer // function is only designed to be used in situations where the event is not strictly necessary (e.g. payment to // another person where the payment is considered to be successful when the other party successfully decrypts a // note). diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer.nr index 2d5007c97bf0..4369189c21ad 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer.nr @@ -18,6 +18,14 @@ unconstrained fn transfer_private() { mint_amount - transfer_amount, ); utils::check_private_balance(env, token_contract_address, recipient, transfer_amount); + + utils::check_private_transfer_event( + env, + token_contract_address, + owner, + recipient, + transfer_amount, + ); } #[test] @@ -32,6 +40,8 @@ unconstrained fn transfer_private_to_self() { // Check balances utils::check_private_balance(env, token_contract_address, owner, mint_amount); + + utils::check_private_transfer_event(env, token_contract_address, owner, owner, transfer_amount); } #[test] @@ -56,6 +66,14 @@ unconstrained fn transfer_private_to_non_deployed_account() { mint_amount - transfer_amount, ); utils::check_private_balance(env, token_contract_address, not_deployed, transfer_amount); + + utils::check_private_transfer_event( + env, + token_contract_address, + owner, + not_deployed, + transfer_amount, + ); } #[test(should_fail_with = "Balance too low")] diff --git a/noir-projects/noir-contracts/contracts/app/token_contract/src/test/utils.nr b/noir-projects/noir-contracts/contracts/app/token_contract/src/test/utils.nr index 67edae1007d3..58ecdddf39b7 100644 --- a/noir-projects/noir-contracts/contracts/app/token_contract/src/test/utils.nr +++ b/noir-projects/noir-contracts/contracts/app/token_contract/src/test/utils.nr @@ -86,6 +86,24 @@ pub unconstrained fn check_private_balance( ); } +pub unconstrained fn check_private_transfer_event( + env: TestEnvironment, + token_contract_address: AztecAddress, + from: AztecAddress, + to: AztecAddress, + amount: u128, +) { + let events = env.get_private_events::(token_contract_address, to); + + assert_eq(events.len(), 1); + + let event = events.get(0); + + assert_eq(event.from, from); + assert_eq(event.to, to); + assert_eq(event.amount, amount); +} + // While authwits authorize a contract to perform an action, spending private notes still requires // knowledge of the note secrets. For private tokens, the note owner must be involved in the // transaction -- they cannot simply give another user an authwit and have that user spend the notes diff --git a/noir-projects/noir-contracts/contracts/test/test_log_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/test_log_contract/src/main.nr index 8e3ce0555304..653e3e8eee18 100644 --- a/noir-projects/noir-contracts/contracts/test/test_log_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/test_log_contract/src/main.nr @@ -6,7 +6,7 @@ pub contract TestLog { macros::{events::event, functions::external, storage::storage}, messages::message_delivery::MessageDelivery, oracle::random::random, - protocol::{address::AztecAddress, traits::{FromField, Serialize}}, + protocol::{address::AztecAddress, traits::{Deserialize, FromField, Serialize}}, state_vars::{Owned, PrivateSet}, }; use field_note::FieldNote; @@ -23,7 +23,7 @@ pub contract TestLog { value3: u8, } - #[derive(Serialize)] + #[derive(Deserialize, Serialize)] struct NestedStruct { a: Field, b: Field, diff --git a/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr b/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr index 082b8af6630f..151e601981d1 100644 --- a/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr +++ b/noir-projects/noir-protocol-circuits/crates/serde/src/serialization.nr @@ -240,7 +240,7 @@ pub comptime fn derive_deserialize(s: TypeDefinition) -> Quoted { let deserialization_of_struct_members = params .map(|(param_name, param_type, _): (Quoted, Type, Quoted)| { quote { - let $param_name = <$param_type as Deserialize>::stream_deserialize(reader); + let $param_name = <$param_type as $crate::serialization::Deserialize>::stream_deserialize(reader); } }) .join(quote {}); @@ -275,7 +275,7 @@ pub comptime fn derive_deserialize(s: TypeDefinition) -> Quoted { fn deserialize(fields: [Field; Self::N]) -> Self { let mut reader = $crate::reader::Reader::new(fields); - let result = Self::stream_deserialize(&mut reader); + let result = ::stream_deserialize(&mut reader); reader.finish(); result } diff --git a/noir/noir-repo b/noir/noir-repo index 39f25c903f0d..7d07e187fb04 160000 --- a/noir/noir-repo +++ b/noir/noir-repo @@ -1 +1 @@ -Subproject commit 39f25c903f0ddc60373ca60a85cbdaa3c0f016cd +Subproject commit 7d07e187fb04d79f5a7cf41501d2c12bc2b1d5d2