diff --git a/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md b/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md index 7d9fbfa86d59..6b3dc2c43775 100644 --- a/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md +++ b/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md @@ -82,14 +82,23 @@ For an unconstrained self-send (the recipient is one of the wallet's own account ### In Noir tests -When testing in Noir, leaving the strategy unset makes `TestEnvironment` fall back to the bare PXE default. Set a strategy when creating the environment to exercise a specific one; it affects message delivery in private executions: +When testing in Noir, leaving the strategy unset makes `TestEnvironment` fall back to the bare PXE default. Set a strategy +when creating the environment to exercise a specific one; it affects message delivery in private executions that use the +default wallet strategy hook. Use `with_default_tag_secret_strategy` to configure the strategy for a specific delivery +mode: ```rust let env = TestEnvironment::new_opts( - TestEnvironmentOptions::new().with_tagging_secret_strategy(TaggingSecretStrategy::non_interactive_handshake()), + TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_unconstrained(), + TaggingSecretStrategy::non_interactive_handshake(), + ), ); ``` +Use `with_default_tag_secret_strategy_all_modes` only when the same strategy should apply to both constrained and +unconstrained delivery. Contract-fixed delivery derivations bypass this default strategy. + ### In production Pass a `resolveTaggingSecretStrategy` hook when [creating the PXE](#configuring-hooks). It receives a `TaggingSecretStrategyRequest` with the executing contract's address and the message's sender, recipient, and delivery mode (`'constrained'` or `'unconstrained'`), so a wallet can apply per-application or per-recipient policies, or surface the decision to the user, instead of returning a fixed value. diff --git a/docs/docs-developers/docs/resources/migration_notes.md b/docs/docs-developers/docs/resources/migration_notes.md index e2a28dc9b975..821acf763150 100644 --- a/docs/docs-developers/docs/resources/migration_notes.md +++ b/docs/docs-developers/docs/resources/migration_notes.md @@ -9,6 +9,15 @@ Aztec is in active development. Each version may introduce breaking changes that ## TBD +### [Aztec.nr] `TestEnvironmentOptions::with_tagging_secret_strategy` replaced + +`TestEnvironmentOptions::with_tagging_secret_strategy` is now `with_default_tag_secret_strategy_all_modes` for tests +that want the same default wallet strategy for both onchain delivery modes. The new naming reflects that these helpers +configure the TXE default wallet strategy hook; contract-fixed delivery derivations bypass that default. + +For mode-specific defaults and hook semantics, see the +[`resolveTaggingSecretStrategy` test helper docs](../foundational-topics/pxe/execution_hooks.md#resolvetaggingsecretstrategy). + ### [Aztec.nr] L1-to-L2 message consumption takes the secret as an array `PrivateContext::consume_l1_to_l2_message` and `PublicContext::consume_l1_to_l2_message` now take the message secret as an arbitrary-length array `[Field; N]` instead of a single `Field`, so a consumer can derive its secret hash from more than one field. The helpers `compute_secret_hash` and `compute_l1_to_l2_message_nullifier` are likewise now generic over the secret length. A single-field secret behaves exactly as before (the hashes are unchanged for `N = 1`) — just wrap it in an array. diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr index 056e5479782e..93ee3b2b3041 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr @@ -180,6 +180,29 @@ mod test { }); } + #[test(should_fail_with = "an unconstrained tagging secret cannot back constrained delivery")] + unconstrained fn constrained_delivery_rejects_a_resolved_unconstrained_secret() { + let env = TestEnvironment::new(); + let secret: Field = 7; + let index: u32 = 0; + + env.private_context(|context| { + mock_existing_handshake_secrets(Option::none()); + let _ = OracleMock::mock("aztec_prv_resolveTaggingStrategy").returns( + ResolvedTaggingStrategy::unconstrained_secret(secret), + ); + let _ = OracleMock::mock("aztec_prv_getNextTaggingIndex").returns(index); + + let _ = derive_log_tag( + context, + OnchainDeliveryMode::onchain_constrained(), + SENDER, + RECIPIENT, + Option::none(), + ); + }); + } + #[test] unconstrained fn constrained_delivery_emits_the_sequence_nullifier_and_constrained_tag() { let env = TestEnvironment::new(); diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/mod.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/mod.nr index 6cd7f18ac34f..1732db45a308 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/mod.nr @@ -13,7 +13,7 @@ pub mod test_environment; quote { private_call_new_flow_oracle }, // TODO: implement once we support more complex types quote { public_call_new_flow_oracle }, // TODO: implement once we support more complex types quote { set_private_txe_context_oracle }, // TODO: implement once we support more complex types - quote { set_tagging_secret_strategy }, // TODO: implement once we support more complex types + quote { set_tagging_secret_strategies }, // TODO: implement once we support more complex types ], )] pub mod txe_oracles; diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/tagging_secret_strategy.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/tagging_secret_strategy.nr index 93f4baa0b0c8..8a694b5b3592 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/tagging_secret_strategy.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/tagging_secret_strategy.nr @@ -1,4 +1,11 @@ -use crate::protocol::{point::EmbeddedCurvePoint, traits::{Deserialize, Serialize}, utils::reader::Reader}; +use crate::keys::ecdh_shared_secret::compute_app_siloed_shared_secret; +use crate::protocol::{ + address::AztecAddress, + hash::poseidon2_hash, + point::EmbeddedCurvePoint, + traits::{Deserialize, Serialize, ToField}, + utils::reader::Reader, +}; global NON_INTERACTIVE_HANDSHAKE: u8 = 1; global ARBITRARY_SECRET: u8 = 2; @@ -7,7 +14,7 @@ global INTERACTIVE_HANDSHAKE: u8 = 4; /// How a message's tagging secret is chosen: the wallet's strategy. /// -/// This type only exists for tests: a Noir test sets it via `with_tagging_secret_strategy` on +/// This type only exists for tests: a Noir test sets it via `with_default_tag_secret_strategy` on /// [`TestEnvironmentOptions`](crate::test::helpers::test_environment::TestEnvironmentOptions). /// Production wallets express the strategy in PXE; the Noir path only consumes the resolved /// [`ResolvedTaggingStrategy`](crate::messages::delivery::ResolvedTaggingStrategy). @@ -45,18 +52,26 @@ impl TaggingSecretStrategy { /// Validates a raw discriminant, as deserialization must always reject unknown values. fn from_parts(kind: u8, secret: EmbeddedCurvePoint) -> Self { let strategy = Self { kind, secret }; + let is_no_payload_strategy = + (kind == NON_INTERACTIVE_HANDSHAKE) | (kind == INTERACTIVE_HANDSHAKE) | (kind == ADDRESS_DERIVED); assert( - ( - ((kind == NON_INTERACTIVE_HANDSHAKE) | (kind == ADDRESS_DERIVED) | (kind == INTERACTIVE_HANDSHAKE)) - & secret.is_infinite() - ) - | (kind == ARBITRARY_SECRET), + (is_no_payload_strategy & secret.is_infinite()) | (kind == ARBITRARY_SECRET), f"unrecognized tagging secret strategy kind: {kind}", ); strategy } } +/// Computes the directional tagging secret PXE derives from an arbitrary shared secret point: the point is app-siloed +/// and the recipient is folded in for direction. Mirrors `AppTaggingSecret.computeDirectional` on the PXE side; tests +/// use it to derive the secret they expect an [`arbitrary_secret`](TaggingSecretStrategy::arbitrary_secret) strategy +/// to produce. +pub fn compute_directional_app_secret(secret: EmbeddedCurvePoint, app: AztecAddress, recipient: AztecAddress) -> Field { + poseidon2_hash( + [compute_app_siloed_shared_secret(secret, app), recipient.to_field()], + ) +} + impl Deserialize for TaggingSecretStrategy { let N: u32 = 3; @@ -81,14 +96,14 @@ mod test { #[test] fn strategy_roundtrips_through_serialization() { let non_interactive = TaggingSecretStrategy::non_interactive_handshake(); + let interactive = TaggingSecretStrategy::interactive_handshake(); let address = TaggingSecretStrategy::address_derived(); let provided = TaggingSecretStrategy::arbitrary_secret(EmbeddedCurvePoint { x: 7, y: 11 }); - let interactive = TaggingSecretStrategy::interactive_handshake(); assert(TaggingSecretStrategy::deserialize(non_interactive.serialize()) == non_interactive); + assert(TaggingSecretStrategy::deserialize(interactive.serialize()) == interactive); assert(TaggingSecretStrategy::deserialize(address.serialize()) == address); assert(TaggingSecretStrategy::deserialize(provided.serialize()) == provided); - assert(TaggingSecretStrategy::deserialize(interactive.serialize()) == interactive); } #[test(should_fail_with = "unrecognized tagging secret strategy kind")] @@ -98,16 +113,22 @@ mod test { #[test(should_fail_with = "unrecognized tagging secret strategy kind")] fn deserializing_handshake_with_noninfinite_point_fails() { - let _ = TaggingSecretStrategy::deserialize([1, 7, 11]); + let _ = TaggingSecretStrategy::deserialize([ + TaggingSecretStrategy::non_interactive_handshake().kind as Field, + 7, + 11, + ]); } #[test(should_fail_with = "unrecognized tagging secret strategy kind")] fn deserializing_address_derived_with_noninfinite_point_fails() { - let _ = TaggingSecretStrategy::deserialize([3, 7, 11]); + let _ = TaggingSecretStrategy::deserialize( + [TaggingSecretStrategy::address_derived().kind as Field, 7, 11], + ); } #[test(should_fail_with = "unrecognized tagging secret strategy kind")] fn deserializing_interactive_handshake_with_noninfinite_point_fails() { - let _ = TaggingSecretStrategy::deserialize([4, 7, 11]); + let _ = TaggingSecretStrategy::deserialize([TaggingSecretStrategy::interactive_handshake().kind as Field, 7, 11]); } } 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 950dae404682..63aaf024292a 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 @@ -13,6 +13,7 @@ use crate::{ event::{event_interface::EventInterface, EventMessage}, hash::{compute_secret_hash, hash_args}, messages::{ + delivery::OnchainDeliveryMode, discovery::{ ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, process_message::process_message_plaintext, }, @@ -88,28 +89,53 @@ pub struct TestEnvironment { /// methods setting each value, e.g.: /// ```noir /// let env = TestEnvironment::new_opts( -/// TestEnvironmentOptions::new().with_tagging_secret_strategy(TaggingSecretStrategy::non_interactive_handshake()), +/// TestEnvironmentOptions::new().with_default_tag_secret_strategy( +/// MessageDelivery::onchain_unconstrained(), +/// TaggingSecretStrategy::non_interactive_handshake(), +/// ), /// ); /// ``` pub struct TestEnvironmentOptions { - tagging_secret_strategy: Option, + default_unconstrained_tagging_secret_strategy: Option, + default_constrained_tagging_secret_strategy: Option, } impl TestEnvironmentOptions { /// Creates a new `TestEnvironmentOptions` with default values. pub fn new() -> Self { - Self { tagging_secret_strategy: Option::none() } + Self { + default_unconstrained_tagging_secret_strategy: Option::none(), + default_constrained_tagging_secret_strategy: Option::none(), + } } - /// Sets the [`TaggingSecretStrategy`] the wallet reports through the + /// Sets the default [`TaggingSecretStrategy`] the wallet reports for `mode` through the /// [`resolve_tagging_strategy`](crate::oracle::resolve_tagging_strategy) oracle, affecting message delivery in - /// private executions. + /// private executions that do not fix their own delivery derivation. /// - /// If not set, the wallet hook is left unconfigured and the private execution environment applies its own default. - pub fn with_tagging_secret_strategy(&mut self, strategy: TaggingSecretStrategy) -> Self { - self.tagging_secret_strategy = Option::some(strategy); + /// If no strategy is set for either mode, the wallet hook is left unconfigured and the private execution + /// environment applies its own default. A mode left unset while the other is configured falls back to the default + /// [`TaggingSecretStrategy::non_interactive_handshake`], the strategy a PXE `resolveTaggingSecretStrategy` hook + /// typically hardcodes for modes it does not customize. + pub fn with_default_tag_secret_strategy(&mut self, mode: M, strategy: TaggingSecretStrategy) -> Self + where + M: Into, + { + if mode.into() == OnchainDeliveryMode::onchain_unconstrained() { + self.default_unconstrained_tagging_secret_strategy = Option::some(strategy); + } else { + self.default_constrained_tagging_secret_strategy = Option::some(strategy); + } *self } + + /// Sets `strategy` for both delivery modes: the equivalent of a PXE `resolveTaggingSecretStrategy` hook that + /// ignores the request's mode. See + /// [`with_default_tag_secret_strategy`](Self::with_default_tag_secret_strategy) to configure a single mode. + pub fn with_default_tag_secret_strategy_all_modes(&mut self, strategy: TaggingSecretStrategy) -> Self { + let _ = self.with_default_tag_secret_strategy(OnchainDeliveryMode::onchain_unconstrained(), strategy); + self.with_default_tag_secret_strategy(OnchainDeliveryMode::onchain_constrained(), strategy) + } } /// Configuration values for [`TestEnvironment::private_context_opts`]. Meant to be used by calling `new` and then @@ -607,18 +633,19 @@ impl TestEnvironment { /// /// ### Sample usage /// ```noir + /// let strategy = TaggingSecretStrategy::non_interactive_handshake(); /// let env = TestEnvironment::new_opts( - /// TestEnvironmentOptions::new().with_tagging_secret_strategy( - /// TaggingSecretStrategy::non_interactive_handshake(), - /// ), + /// TestEnvironmentOptions::new().with_default_tag_secret_strategy_all_modes(strategy), /// ); /// ``` pub unconstrained fn new_opts(options: TestEnvironmentOptions) -> Self { assert_compatible_oracle_version(); txe_oracles::assert_compatible_txe_oracle_version(); - // Forward the configured strategy to the wallet. - txe_oracles::set_tagging_secret_strategy(options.tagging_secret_strategy); + txe_oracles::set_tagging_secret_strategies( + options.default_unconstrained_tagging_secret_strategy, + options.default_constrained_tagging_secret_strategy, + ); Self { // Use an offset to avoid secret collision with account secrets. Without this, when diff --git a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/resolve_tagging_strategy.nr b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/resolve_tagging_strategy.nr index b02994d2e6ca..e2aed6337206 100644 --- a/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/resolve_tagging_strategy.nr +++ b/noir-projects/aztec-nr/aztec/src/test/helpers/test_environment/test/resolve_tagging_strategy.nr @@ -1,14 +1,56 @@ use crate::{ - keys::ecdh_shared_secret::compute_app_siloed_shared_secret, - messages::delivery::{OnchainDeliveryMode, ResolvedTaggingStrategy}, + messages::delivery::{MessageDelivery, OnchainDeliveryMode, ResolvedTaggingStrategy}, oracle::resolve_tagging_strategy::resolve_tagging_strategy, - protocol::{address::AztecAddress, hash::poseidon2_hash, point::EmbeddedCurvePoint, traits::{FromField, ToField}}, + protocol::{address::AztecAddress, point::EmbeddedCurvePoint, traits::FromField}, test::helpers::{ - tagging_secret_strategy::TaggingSecretStrategy, + tagging_secret_strategy::{compute_directional_app_secret, TaggingSecretStrategy}, test_environment::{TestEnvironment, TestEnvironmentOptions}, }, }; +#[test] +unconstrained fn an_unset_constrained_mode_falls_back_to_the_default_strategy() { + // Only the unconstrained mode is configured: the constrained mode falls back to the default non-interactive + // handshake strategy. + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_unconstrained(), + TaggingSecretStrategy::arbitrary_secret(EmbeddedCurvePoint { x: 7, y: 11 }), + )); + let app = env.create_contract_account(); + let recipient = env.create_light_account(); + + env.private_context_at(app, |_| { + let resolved = resolve_tagging_strategy( + AztecAddress::from_field(1), + recipient, + OnchainDeliveryMode::onchain_constrained(), + ); + assert_eq(resolved, ResolvedTaggingStrategy::non_interactive_handshake()); + }); +} + +#[test] +unconstrained fn an_unset_unconstrained_mode_falls_back_to_the_default_strategy() { + // Only the constrained mode is configured: the unconstrained mode falls back to the default non-interactive + // handshake strategy for an external recipient. + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_constrained(), + TaggingSecretStrategy::interactive_handshake(), + )); + let app = env.create_contract_account(); + let sender = env.create_light_account(); + let recipient = AztecAddress::from_field(8); + + env.private_context_at(app, |_| { + let resolved = resolve_tagging_strategy( + sender, + recipient, + OnchainDeliveryMode::onchain_unconstrained(), + ); + assert_eq(resolved, ResolvedTaggingStrategy::non_interactive_handshake()); + }); +} + #[test] unconstrained fn defaults_an_unconstrained_self_send_to_the_address_derived_shared_secret() { let mut env = TestEnvironment::new(); @@ -69,10 +111,9 @@ unconstrained fn defaults_constrained_delivery_to_a_non_interactive_handshake() #[test] unconstrained fn constrained_delivery_succeeds_with_a_configured_strategy() { - let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_tagging_secret_strategy( + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy_all_modes( TaggingSecretStrategy::non_interactive_handshake(), )); - // The hook path looks up the executing contract's class ID, so run in a deployed contract's context. let app = env.create_contract_account(); let recipient = env.create_light_account(); @@ -88,12 +129,12 @@ unconstrained fn constrained_delivery_succeeds_with_a_configured_strategy() { #[test] unconstrained fn applies_a_configured_interactive_handshake_strategy() { - let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_tagging_secret_strategy( + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_constrained(), TaggingSecretStrategy::interactive_handshake(), )); - // The hook path looks up the executing contract's class ID, so run in a deployed contract's context. let app = env.create_contract_account(); - let recipient = AztecAddress::from_field(8); + let recipient = env.create_light_account(); env.private_context_at(app, |_| { let resolved = resolve_tagging_strategy( @@ -107,10 +148,10 @@ unconstrained fn applies_a_configured_interactive_handshake_strategy() { #[test] unconstrained fn applies_the_strategy_set_in_the_options() { - let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_tagging_secret_strategy( + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_unconstrained(), TaggingSecretStrategy::arbitrary_secret(EmbeddedCurvePoint { x: 7, y: 11 }), )); - // The hook path looks up the executing contract's class ID, so run in a deployed contract's context. let app = env.create_contract_account(); let recipient = AztecAddress::from_field(8); @@ -126,33 +167,40 @@ unconstrained fn applies_the_strategy_set_in_the_options() { #[test] unconstrained fn app_silos_an_arbitrary_secret_point() { + // Both delivery modes resolve the configured arbitrary secret to the same app-siloed point. Mode-blindness + // is deliberate: PXE's tagging-strategy hook resolves whatever the wallet configured regardless of mode, and + // the TXE oracle must match that behavior. Constrained delivery rejects unsound secrets later, not here. let point = EmbeddedCurvePoint { x: 7, y: 11 }; - let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_tagging_secret_strategy( + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy_all_modes( TaggingSecretStrategy::arbitrary_secret(point), )); - // The hook path looks up the executing contract's class ID, so run in a deployed contract's context. let app = env.create_contract_account(); let recipient = AztecAddress::from_field(8); env.private_context_at(app, |_| { - let resolved = resolve_tagging_strategy( + let expected_secret = compute_directional_app_secret(point, app, recipient); + + let resolved_unconstrained = resolve_tagging_strategy( AztecAddress::from_field(1), recipient, OnchainDeliveryMode::onchain_unconstrained(), ); + assert_eq(resolved_unconstrained, ResolvedTaggingStrategy::unconstrained_secret(expected_secret)); - // PXE app-silos the raw point before handing it over, then folds in the recipient for direction. This mirrors - // `AppTaggingSecret.computeDirectional` on the PXE side. - let app_secret = compute_app_siloed_shared_secret(point, app); - let expected_secret = poseidon2_hash([app_secret, recipient.to_field()]); - assert_eq(resolved, ResolvedTaggingStrategy::unconstrained_secret(expected_secret)); + let resolved_constrained = resolve_tagging_strategy( + AztecAddress::from_field(1), + recipient, + OnchainDeliveryMode::onchain_constrained(), + ); + assert_eq(resolved_constrained, ResolvedTaggingStrategy::unconstrained_secret(expected_secret)); }); } #[test] unconstrained fn overrides_a_configured_arbitrary_secret_for_an_unconstrained_self_send() { let point = EmbeddedCurvePoint { x: 7, y: 11 }; - let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_tagging_secret_strategy( + let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_unconstrained(), TaggingSecretStrategy::arbitrary_secret(point), )); let app = env.create_contract_account(); @@ -168,8 +216,7 @@ unconstrained fn overrides_a_configured_arbitrary_secret_for_an_unconstrained_se // A self-send forces an address-derived secret even with an arbitrary secret configured. The resolved // secret is unconstrained, but not the app-siloed arbitrary point the configured secret would have produced. - let app_secret = compute_app_siloed_shared_secret(point, app); - let arbitrary_secret = poseidon2_hash([app_secret, recipient.to_field()]); + let arbitrary_secret = compute_directional_app_secret(point, app, recipient); assert(resolved.is_unconstrained_secret()); assert(resolved != ResolvedTaggingStrategy::unconstrained_secret(arbitrary_secret)); }); 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 ad6a98ffcaae..55bb5c56fdd8 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 @@ -21,8 +21,8 @@ use crate::protocol::{ /// [`oracle::version`](crate::oracle::version), which covers oracles used during contract execution by PXE. /// /// The TypeScript counterparts are in `yarn-project/txe/src/txe_oracle_version.ts`. -pub global TXE_ORACLE_VERSION_MAJOR: Field = 2; -pub global TXE_ORACLE_VERSION_MINOR: Field = 3; +pub global TXE_ORACLE_VERSION_MAJOR: Field = 3; +pub global TXE_ORACLE_VERSION_MINOR: Field = 0; /// Asserts that the TXE oracle interface version is compatible. pub unconstrained fn assert_compatible_txe_oracle_version() { @@ -265,8 +265,11 @@ pub unconstrained fn send_l1_to_l2_message( recipient: AztecAddress, ) -> Field {} -#[oracle(aztec_txe_setTaggingSecretStrategy)] -pub unconstrained fn set_tagging_secret_strategy(strategy: Option) {} +#[oracle(aztec_txe_setTaggingSecretStrategies)] +pub unconstrained fn set_tagging_secret_strategies( + unconstrained_strategy: Option, + constrained_strategy: Option, +) {} #[oracle(aztec_txe_privateCallNewFlow)] unconstrained fn private_call_new_flow_oracle( diff --git a/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/main.nr index 1fa646cdc699..1377f7466025 100644 --- a/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/main.nr @@ -1,4 +1,4 @@ -//! Thin wrappers around onchain message delivery for tests. +//! Thin wrappers around onchain message delivery for TXE tests. use aztec::macros::aztec; mod test; @@ -7,7 +7,7 @@ mod test; pub contract OnchainDeliveryTest { use aztec::{ macros::{events::event, functions::external, storage::storage}, - messages::delivery::MessageDelivery, + messages::delivery::{MessageDelivery, OnchainDeliveryMode}, note::note_viewer_options::NoteViewerOptions, oracle::notes::get_next_tagging_index, protocol::{ @@ -34,10 +34,10 @@ pub contract OnchainDeliveryTest { } #[external("private")] - fn next_index_for_secret(secret: Field) -> u32 { + fn next_index_for_secret(secret: Field, mode: OnchainDeliveryMode) -> u32 { // Safety: test-only observation of the index the PXE hands out next for this secret. unsafe { - get_next_tagging_index(secret, MessageDelivery::onchain_constrained()) + get_next_tagging_index(secret, mode) } } diff --git a/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/test.nr b/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/test.nr index 32b62ead583d..8a0fa818b7c5 100644 --- a/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/test/onchain_delivery_test_contract/src/test.nr @@ -1,20 +1,33 @@ -//! Tests for constrained delivery through the public message-delivery API. +//! Tests for onchain message delivery through the public message-delivery API. //! -//! These tests exercise the sender-side helper flow indirectly via constrained note/event delivery: resolve or +//! The constrained tests exercise the sender-side helper flow indirectly via note/event delivery: resolve or //! bootstrap the app-siloed handshake secret, reserve the next per-secret index, constrain that index, emit the //! sequence nullifier, then emit the tagged private log. The malformed-log test lands a constrained tag without its -//! sequence nullifier so the next real delivery must reject the broken sequence. +//! sequence nullifier so the next real delivery must reject the broken sequence. The unconstrained tests cover +//! delivery in the mode without sequence nullifiers, alongside and independent of the constrained flow. use crate::OnchainDeliveryTest; use aztec::{ - protocol::address::AztecAddress, + messages::delivery::MessageDelivery, + protocol::{address::AztecAddress, point::EmbeddedCurvePoint, traits::FromField}, standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS, - test::helpers::test_environment::{CallPrivateOptions, DeployOptions, ExecuteUtilityOptions, TestEnvironment}, + test::helpers::{ + tagging_secret_strategy::{compute_directional_app_secret, TaggingSecretStrategy}, + test_environment::{ + CallPrivateOptions, DeployOptions, ExecuteUtilityOptions, TestEnvironment, TestEnvironmentOptions, + }, + }, }; use handshake_registry_contract::HandshakeRegistry; unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { - let mut env = TestEnvironment::new(); + setup_opts(TestEnvironmentOptions::new()) +} + +unconstrained fn setup_opts( + options: TestEnvironmentOptions, +) -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { + let mut env = TestEnvironment::new_opts(options); let sender = env.create_light_account(); let recipient = env.create_light_account(); @@ -35,6 +48,62 @@ unconstrained fn authorizing(registry_address: AztecAddress) -> CallPrivateOptio CallPrivateOptions::new().with_authorized_utility_call_targets([registry_address]) } +unconstrained fn external_recipient() -> AztecAddress { + AztecAddress::from_field(8) +} + +#[test] +unconstrained fn unconstrained_delivery_works_alongside_constrained_delivery() { + let point = EmbeddedCurvePoint { + x: 0x15d55a5b3b2caa6a6207f313f05c5113deba5da9927d6421bcaa164822b911bc, + y: 0x0974c3d0825031ae933243d653ebb1a0b08b90ee7f228f94c5c74739ea3c871e, + }; + // Only the unconstrained mode is configured: the constrained mode falls back to the default non-interactive + // handshake strategy, so the constrained delivery below still bootstraps a registry handshake. + let options = TestEnvironmentOptions::new().with_default_tag_secret_strategy( + MessageDelivery::onchain_unconstrained(), + TaggingSecretStrategy::arbitrary_secret(point), + ); + let (env, registry_address, test_address, sender, _) = setup_opts(options); + let recipient = external_recipient(); + let test_contract = OnchainDeliveryTest::at(test_address); + let registry = HandshakeRegistry::at(registry_address); + + env.call_private(sender, test_contract.emit_event_unconstrained(recipient, 1)); + + let expected_unconstrained_secret = compute_directional_app_secret(point, test_address, recipient); + let next_unconstrained_index = env.call_private( + sender, + test_contract.next_index_for_secret( + expected_unconstrained_secret, + MessageDelivery::onchain_unconstrained().into(), + ), + ); + assert_eq(next_unconstrained_index, 1); + + let maybe_handshake = env.execute_utility_opts( + ExecuteUtilityOptions::new().with_from(test_address), + registry.get_app_siloed_secrets(sender, recipient), + ); + assert(maybe_handshake.is_none()); + + env.call_private_opts(sender, authorizing(registry_address), test_contract.emit_event(recipient, 1)); + + let handshake = env + .execute_utility_opts( + ExecuteUtilityOptions::new().with_from(test_address), + registry.get_app_siloed_secrets(sender, recipient), + ) + .expect(f"constrained delivery should have stored a handshake"); + assert(handshake.shared != expected_unconstrained_secret); + + let next_constrained_index = env.call_private( + sender, + test_contract.next_index_for_secret(handshake.shared, MessageDelivery::onchain_constrained().into()), + ); + assert_eq(next_constrained_index, 1); +} + #[test] unconstrained fn rehandshake_replaces_registry_secret_for_future_delivery() { let (env, registry_address, test_address, sender, recipient) = setup(); @@ -62,7 +131,10 @@ unconstrained fn rehandshake_replaces_registry_secret_for_future_delivery() { env.call_private_opts(sender, authorizing(registry_address), test_contract.emit_event(recipient, 1)); - let next_index = env.call_private(sender, test_contract.next_index_for_secret(second_secret)); + let next_index = env.call_private( + sender, + test_contract.next_index_for_secret(second_secret, MessageDelivery::onchain_constrained().into()), + ); assert_eq(next_index, 1); } @@ -104,7 +176,10 @@ unconstrained fn interactive_handshake_reuses_an_existing_registry_handshake() { test_contract.emit_event_via_interactive_handshake(recipient, 1), ); - let next_index = env.call_private(sender, test_contract.next_index_for_secret(secret)); + let next_index = env.call_private( + sender, + test_contract.next_index_for_secret(secret, MessageDelivery::onchain_constrained().into()), + ); assert_eq(next_index, 1); } diff --git a/yarn-project/end-to-end/src/automine/delivery/constrained.test.ts b/yarn-project/end-to-end/src/automine/delivery/constrained.test.ts index d3c1b1bef8ce..7f9691480faf 100644 --- a/yarn-project/end-to-end/src/automine/delivery/constrained.test.ts +++ b/yarn-project/end-to-end/src/automine/delivery/constrained.test.ts @@ -11,6 +11,9 @@ import { jest } from '@jest/globals'; import { AUTOMINE_E2E_OPTS } from '../../fixtures/fixtures.js'; import { ensureHandshakeRegistryPublished, setup } from '../../fixtures/setup.js'; +// Keep in sync with aztec::messages::delivery::OnchainDeliveryMode. +const ONCHAIN_CONSTRAINED_DELIVERY_MODE = { inner: 3 }; + // Delivery-method-specific tests that don't fit the generic (strategy, mode) matrix in `onchain.test.ts` describe('delivery/constrained', () => { jest.setTimeout(300_000); @@ -56,7 +59,9 @@ describe('delivery/constrained', () => { // The second send reuses the handshake rather than bootstrapping a new one: the secret is unchanged. expect(secret).toEqual(secretAfterFirstSend); - const { result: index } = await contract.methods.next_index_for_secret(secret).simulate({ from: sender }); + const { result: index } = await contract.methods + .next_index_for_secret(secret, ONCHAIN_CONSTRAINED_DELIVERY_MODE) + .simulate({ from: sender }); expect(index).toEqual(2n); }); @@ -92,7 +97,9 @@ describe('delivery/constrained', () => { .simulate({ from: sender }); expect(secret).toBeDefined(); - const { result: index } = await contract.methods.next_index_for_secret(secret).simulate({ from: sender }); + const { result: index } = await contract.methods + .next_index_for_secret(secret, ONCHAIN_CONSTRAINED_DELIVERY_MODE) + .simulate({ from: sender }); expect(index).toEqual(2n); }); @@ -111,7 +118,9 @@ describe('delivery/constrained', () => { .simulate({ from: sender }); expect(secret).toBeDefined(); - const { result: index } = await contract.methods.next_index_for_secret(secret).simulate({ from: sender }); + const { result: index } = await contract.methods + .next_index_for_secret(secret, ONCHAIN_CONSTRAINED_DELIVERY_MODE) + .simulate({ from: sender }); expect(index).toEqual(2n); }); @@ -128,7 +137,9 @@ describe('delivery/constrained', () => { .simulate({ from: sender }); expect(secret).toBeDefined(); - const { result: index } = await contract.methods.next_index_for_secret(secret).simulate({ from: sender }); + const { result: index } = await contract.methods + .next_index_for_secret(secret, ONCHAIN_CONSTRAINED_DELIVERY_MODE) + .simulate({ from: sender }); expect(index).toEqual(1n); }); @@ -146,7 +157,9 @@ describe('delivery/constrained', () => { .simulate({ from: sender }); expect(secret).toBeDefined(); - const { result: index } = await contract.methods.next_index_for_secret(secret).simulate({ from: sender }); + const { result: index } = await contract.methods + .next_index_for_secret(secret, ONCHAIN_CONSTRAINED_DELIVERY_MODE) + .simulate({ from: sender }); expect(index).toEqual(1n); }); }); diff --git a/yarn-project/pxe/src/contract_function_simulator/noir-structs/resolved_tagging_strategy.ts b/yarn-project/pxe/src/contract_function_simulator/noir-structs/resolved_tagging_strategy.ts index 8b1930281d3e..578ec805c6dd 100644 --- a/yarn-project/pxe/src/contract_function_simulator/noir-structs/resolved_tagging_strategy.ts +++ b/yarn-project/pxe/src/contract_function_simulator/noir-structs/resolved_tagging_strategy.ts @@ -47,8 +47,10 @@ export function resolvedTaggingStrategyToFields(resolved: ResolvedTaggingStrateg export function resolvedTaggingStrategyFromFields(kind: number, secret: Fr): ResolvedTaggingStrategy { switch (kind) { case NON_INTERACTIVE_HANDSHAKE: + assertAbsentSecret(kind, secret); return { type: 'non-interactive-handshake' }; case INTERACTIVE_HANDSHAKE: + assertAbsentSecret(kind, secret); return { type: 'interactive-handshake' }; case UNCONSTRAINED_SECRET: return { type: 'unconstrained-secret', secret }; @@ -56,3 +58,9 @@ export function resolvedTaggingStrategyFromFields(kind: number, secret: Fr): Res throw new Error(`Unrecognized resolved tagging strategy kind: ${kind}`); } } + +function assertAbsentSecret(kind: number, secret: Fr): void { + if (!secret.isZero()) { + throw new Error(`Resolved tagging strategy ${kind} must not include a secret`); + } +} diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.test.ts index ee6405fd1b3f..23ca2fbc3108 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/oracle_type_mappings.test.ts @@ -8,6 +8,7 @@ import { EphemeralArrayService } from '../ephemeral_array_service.js'; import { BoundedVec } from '../noir-structs/bounded_vec.js'; import { type LogRetrievalRequest, LogSource } from '../noir-structs/log_retrieval_request.js'; import { Option } from '../noir-structs/option.js'; +import type { ResolvedTaggingStrategy } from '../noir-structs/resolved_tagging_strategy.js'; import { ARRAY, AZTEC_ADDRESS, @@ -21,6 +22,7 @@ import { OPTION, POINT, PROVIDED_SECRET, + RESOLVED_TAGGING_STRATEGY, type SlotShape, type TypeMapping, U32, @@ -107,6 +109,25 @@ describe('oracle type mappings', () => { }); }); + describe('RESOLVED_TAGGING_STRATEGY', () => { + const secret = new Fr(42); + + it('rejects an unknown strategy kind', () => { + expect(() => deserializeStrategy(new Fr(99), Fr.ZERO)).toThrow('Unrecognized resolved tagging strategy kind'); + }); + + it.each([ + ['non-interactive handshake', new Fr(1)], + ['interactive handshake', new Fr(3)], + ])('rejects %s with a nonzero secret', (_name, kind) => { + expect(() => deserializeStrategy(kind, secret)).toThrow(`Resolved tagging strategy ${kind.toNumber()}`); + }); + + function deserializeStrategy(kind: Fr, secret: Fr): ResolvedTaggingStrategy { + return RESOLVED_TAGGING_STRATEGY.deserialization!.fn([new FieldReader([kind]), new FieldReader([secret])]); + } + }); + describe('AZTEC_ADDRESS', () => { it('serializes to its declared shape', async () => { expect(shapeOf(AZTEC_ADDRESS.serialization!.fn(await AztecAddress.random()))).toEqual(AZTEC_ADDRESS.shape); diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts index e8158f1f36cd..f27f07adf5e0 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.ts @@ -30,9 +30,10 @@ import { } from '@aztec/stdlib/tx'; import type { ResolveCustomRequest } from '../../hooks/resolve_custom_request.js'; -import type { - ResolveTaggingSecretStrategy, - TaggingSecretStrategy, +import { + DEFAULT_TAGGING_SECRET_STRATEGY, + type ResolveTaggingSecretStrategy, + type TaggingSecretStrategy, } from '../../hooks/resolve_tagging_secret_strategy.js'; import { NoteService } from '../../notes/note_service.js'; import type { SenderTaggingStore } from '../../storage/tagging_store/sender_tagging_store.js'; @@ -233,8 +234,7 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP ): Promise { const hook: ResolveTaggingSecretStrategy | undefined = this.hooks?.resolveTaggingSecretStrategy; if (!hook) { - // With no hook, both delivery modes default to a non-interactive handshake - return { type: 'non-interactive-handshake' }; + return DEFAULT_TAGGING_SECRET_STRATEGY; } const contractClassId = await this.#getCurrentContractClassId(this.contractAddress); diff --git a/yarn-project/pxe/src/hooks/index.ts b/yarn-project/pxe/src/hooks/index.ts index 360cae3e904f..029b85c5e000 100644 --- a/yarn-project/pxe/src/hooks/index.ts +++ b/yarn-project/pxe/src/hooks/index.ts @@ -6,6 +6,7 @@ export type { export { type ExecutionHooks, composeHooks } from './execution_hooks.js'; export { type CustomRequest, type ResolveCustomRequest } from './resolve_custom_request.js'; export { + DEFAULT_TAGGING_SECRET_STRATEGY, type ResolveTaggingSecretStrategy, type TaggingSecretStrategy, type TaggingSecretStrategyRequest, diff --git a/yarn-project/pxe/src/hooks/resolve_tagging_secret_strategy.ts b/yarn-project/pxe/src/hooks/resolve_tagging_secret_strategy.ts index 6ca37cc05e68..fdfd3e8e0f25 100644 --- a/yarn-project/pxe/src/hooks/resolve_tagging_secret_strategy.ts +++ b/yarn-project/pxe/src/hooks/resolve_tagging_secret_strategy.ts @@ -39,6 +39,9 @@ export type TaggingSecretStrategy = secret: Point; }; +/** The strategy PXE applies to both delivery modes when no `resolveTaggingSecretStrategy` hook is configured. */ +export const DEFAULT_TAGGING_SECRET_STRATEGY: TaggingSecretStrategy = { type: 'non-interactive-handshake' }; + /** Information about the message delivery requesting a tagging secret strategy. */ export type TaggingSecretStrategyRequest = { contractAddress: AztecAddress; diff --git a/yarn-project/txe/src/oracle/interfaces.ts b/yarn-project/txe/src/oracle/interfaces.ts index b1d29f3f18cc..50eca629ea36 100644 --- a/yarn-project/txe/src/oracle/interfaces.ts +++ b/yarn-project/txe/src/oracle/interfaces.ts @@ -73,7 +73,14 @@ export interface ITxeExecutionOracle { addAccount(secret: Fr): Promise; addAuthWitness(address: AztecAddress, messageHash: Fr): Promise; sendL1ToL2Message(content: Fr, secretHash: Fr, sender: EthAddress, recipient: AztecAddress): Promise; - setTaggingSecretStrategy(strategy: Option): void; + /** + * Configures the tagging secret strategy the test's simulated wallet resolves for each delivery mode. A `none` + * clears that mode, so it falls back to the default strategy (or, when both modes end up unset, to no hook at all). + */ + setTaggingSecretStrategies( + unconstrainedStrategy: Option, + constrainedStrategy: Option, + ): void; getLastBlockTimestamp(): Promise; getLastTxEffects(): Promise<{ txHash: TxHash; diff --git a/yarn-project/txe/src/oracle/tagging_secret_strategy.test.ts b/yarn-project/txe/src/oracle/tagging_secret_strategy.test.ts new file mode 100644 index 000000000000..ef2f03e2b97a --- /dev/null +++ b/yarn-project/txe/src/oracle/tagging_secret_strategy.test.ts @@ -0,0 +1,47 @@ +import { Fr } from '@aztec/foundation/curves/bn254'; +import { Point } from '@aztec/foundation/curves/grumpkin'; +import { DEFAULT_TAGGING_SECRET_STRATEGY, type TaggingSecretStrategy } from '@aztec/pxe/server'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { AppTaggingSecretKind } from '@aztec/stdlib/logs'; + +import { makeResolveTaggingSecretStrategyHook } from './tagging_secret_strategy.js'; + +describe('makeResolveTaggingSecretStrategyHook', () => { + it('returns undefined when no TXE strategy is configured', () => { + expect(makeResolveTaggingSecretStrategyHook(new Map())).toBeUndefined(); + }); + + it('selects a strategy by delivery mode', async () => { + const unconstrained: TaggingSecretStrategy = { type: 'arbitrary-secret', secret: await Point.random() }; + const constrained: TaggingSecretStrategy = { type: 'interactive-handshake' }; + const hook = makeResolveTaggingSecretStrategyHook( + new Map([ + [AppTaggingSecretKind.UNCONSTRAINED, unconstrained], + [AppTaggingSecretKind.CONSTRAINED, constrained], + ]), + ); + + await expect(hook?.(makeRequest(AppTaggingSecretKind.UNCONSTRAINED))).resolves.toBe(unconstrained); + await expect(hook?.(makeRequest(AppTaggingSecretKind.CONSTRAINED))).resolves.toBe(constrained); + }); + + it('defaults an unset mode to PXE default strategy when another mode is configured', async () => { + const unconstrained = { type: 'address-derived' as const }; + const hook = makeResolveTaggingSecretStrategyHook(new Map([[AppTaggingSecretKind.UNCONSTRAINED, unconstrained]])); + + await expect(hook?.(makeRequest(AppTaggingSecretKind.UNCONSTRAINED))).resolves.toBe(unconstrained); + await expect(hook?.(makeRequest(AppTaggingSecretKind.CONSTRAINED))).resolves.toEqual( + DEFAULT_TAGGING_SECRET_STRATEGY, + ); + }); +}); + +function makeRequest(deliveryMode: AppTaggingSecretKind) { + return { + contractAddress: AztecAddress.ZERO, + contractClassId: Fr.ZERO, + sender: AztecAddress.ZERO, + recipient: AztecAddress.ZERO, + deliveryMode, + }; +} diff --git a/yarn-project/txe/src/oracle/tagging_secret_strategy.ts b/yarn-project/txe/src/oracle/tagging_secret_strategy.ts new file mode 100644 index 000000000000..089ae0d78ff8 --- /dev/null +++ b/yarn-project/txe/src/oracle/tagging_secret_strategy.ts @@ -0,0 +1,25 @@ +import { + DEFAULT_TAGGING_SECRET_STRATEGY, + type ResolveTaggingSecretStrategy, + type TaggingSecretStrategy, +} from '@aztec/pxe/server'; +import type { AppTaggingSecretKind } from '@aztec/stdlib/logs'; + +/** The tagging secret strategies a TXE test has configured, keyed by delivery mode. Absence means "not configured". */ +export type TXETaggingSecretStrategies = Map; + +/** + * Builds the `resolveTaggingSecretStrategy` hook backing the `aztec_txe_setTaggingSecretStrategies` oracle. Returns + * `undefined` when no strategy is configured, so PXE's own no-hook default path is exercised. When at least one mode + * is configured, modes without an entry resolve to {@link DEFAULT_TAGGING_SECRET_STRATEGY}, matching what PXE would + * apply without a hook. + */ +export function makeResolveTaggingSecretStrategyHook( + strategies: TXETaggingSecretStrategies, +): ResolveTaggingSecretStrategy | undefined { + if (strategies.size === 0) { + return undefined; + } + + return ({ deliveryMode }) => Promise.resolve(strategies.get(deliveryMode) ?? DEFAULT_TAGGING_SECRET_STRATEGY); +} diff --git a/yarn-project/txe/src/oracle/txe_oracle_registry.ts b/yarn-project/txe/src/oracle/txe_oracle_registry.ts index 63728243cbd9..162c0ac2e10d 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_registry.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_registry.ts @@ -67,7 +67,7 @@ const GAS_SETTINGS: TypeMapping = { }; // Tagging secret strategy discriminants. Must match the Noir test helper `TaggingSecretStrategy` in -// aztec-nr `test/helpers/tagging_secret_strategy.nr`. This is a test-only oracle (only `set_tagging_secret_strategy` +// aztec-nr `test/helpers/tagging_secret_strategy.nr`. This is a test-only oracle (only `set_tagging_secret_strategies` // reads it), so the mapping lives here on the TXE side rather than in the production oracle type mappings. const STRATEGY_NON_INTERACTIVE_HANDSHAKE = 1; const STRATEGY_ARBITRARY_SECRET = 2; @@ -320,8 +320,11 @@ export const TXE_ORACLE_REGISTRY = { returnType: FIELD, }), - aztec_txe_setTaggingSecretStrategy: makeEntry({ - params: [{ name: 'strategy', type: OPTION(TAGGING_SECRET_STRATEGY) }], + aztec_txe_setTaggingSecretStrategies: makeEntry({ + params: [ + { name: 'unconstrainedStrategy', type: OPTION(TAGGING_SECRET_STRATEGY) }, + { name: 'constrainedStrategy', type: OPTION(TAGGING_SECRET_STRATEGY) }, + ], }), aztec_txe_getLastBlockTimestamp: makeEntry({ diff --git a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts index 9c86619698ab..6bd2b4e31d74 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_top_level_context.ts @@ -73,6 +73,7 @@ import { } from '@aztec/stdlib/kernel'; import { deriveKeys, hashPublicKey } from '@aztec/stdlib/keys'; import type { PrivateLog } from '@aztec/stdlib/logs'; +import { AppTaggingSecretKind } from '@aztec/stdlib/logs'; import { L1Actor, L1ToL2Message, L2Actor } from '@aztec/stdlib/messaging'; import { ChonkProof } from '@aztec/stdlib/proofs'; import { makeGlobalVariables } from '@aztec/stdlib/testing'; @@ -99,6 +100,7 @@ import type { TXEAccountStore } from '../utils/txe_account_store.js'; import type { TXEArtifactResolver } from '../utils/txe_artifact_resolver.js'; import { TXEPublicContractDataSource } from '../utils/txe_public_contract_data_source.js'; import type { ITxeExecutionOracle } from './interfaces.js'; +import { type TXETaggingSecretStrategies, makeResolveTaggingSecretStrategyHook } from './tagging_secret_strategy.js'; export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracle { isMisc = true as const; @@ -123,7 +125,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl private version: Fr, private chainId: Fr, private authwits: Map, - private taggingSecretStrategy: TaggingSecretStrategy | undefined, + private taggingSecretStrategies: TXETaggingSecretStrategies, private readonly artifactResolver: TXEArtifactResolver, private readonly rootPath: string, private readonly packageName: string, @@ -356,8 +358,19 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl this.authwits.set(authWitness.requestHash.toString(), authWitness); } - setTaggingSecretStrategy(strategy: Option): void { - this.taggingSecretStrategy = strategy.value; + setTaggingSecretStrategies( + unconstrainedStrategy: Option, + constrainedStrategy: Option, + ): void { + const apply = (mode: AppTaggingSecretKind, strategy: Option) => { + if (strategy.isSome()) { + this.taggingSecretStrategies.set(mode, strategy.value); + } else { + this.taggingSecretStrategies.delete(mode); + } + }; + apply(AppTaggingSecretKind.UNCONSTRAINED, unconstrainedStrategy); + apply(AppTaggingSecretKind.CONSTRAINED, constrainedStrategy); } async sendL1ToL2Message(content: Fr, secretHash: Fr, sender: EthAddress, recipient: AztecAddress): Promise { @@ -471,7 +484,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl const simulator = new WASMSimulator(); const transientArrayService = new TransientArrayService(); - const taggingSecretStrategy = this.taggingSecretStrategy; const privateExecutionOracle = new PrivateExecutionOracle({ argsHash, txContext, @@ -510,9 +522,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl isStaticCall ? 'private view' : 'private', authorizedUtilityCallTargets, ), - // Only configure the hook when a strategy was explicitly set, so that otherwise the default tagging secret - // strategy is exercised. - resolveTaggingSecretStrategy: taggingSecretStrategy ? () => Promise.resolve(taggingSecretStrategy) : undefined, + resolveTaggingSecretStrategy: makeResolveTaggingSecretStrategyHook(this.taggingSecretStrategies), }), transientArrayService, }); @@ -964,9 +974,9 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl } } - close(): [bigint, Map, TaggingSecretStrategy | undefined] { + close(): [bigint, Map, TXETaggingSecretStrategies] { this.logger.debug('Exiting Top Level Context'); - return [this.nextBlockTimestamp, this.authwits, this.taggingSecretStrategy]; + return [this.nextBlockTimestamp, this.authwits, this.taggingSecretStrategies]; } private async getLastBlockNumber(): Promise { diff --git a/yarn-project/txe/src/oracle/txe_oracle_version.ts b/yarn-project/txe/src/oracle/txe_oracle_version.ts index 6a998bbdf15b..4b221cac6814 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_version.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_version.ts @@ -5,8 +5,8 @@ * * The Noir counterparts are in `noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr`. */ -export const TXE_ORACLE_VERSION_MAJOR = 2; -export const TXE_ORACLE_VERSION_MINOR = 3; +export const TXE_ORACLE_VERSION_MAJOR = 3; +export const TXE_ORACLE_VERSION_MINOR = 0; /** * This hash is computed from the TXE oracle interfaces (IAvmExecutionOracle and ITxeExecutionOracle) and is used to @@ -14,4 +14,4 @@ export const TXE_ORACLE_VERSION_MINOR = 3; * - TXE_ORACLE_VERSION_MAJOR (and reset MINOR to 0) for breaking changes, or * - TXE_ORACLE_VERSION_MINOR for additive changes (new oracle method added). */ -export const TXE_ORACLE_INTERFACE_HASH = '4ed3618087fafb4aa63c6580996a69bf6bc257844035a2019692586b5b8daf34'; +export const TXE_ORACLE_INTERFACE_HASH = 'bea75bd85baebd7aa1438efb1defab496d27540bc805a8d854bb74b8d45fd859'; diff --git a/yarn-project/txe/src/rpc_translator.ts b/yarn-project/txe/src/rpc_translator.ts index a83ea031345f..0a8cbb71759f 100644 --- a/yarn-project/txe/src/rpc_translator.ts +++ b/yarn-project/txe/src/rpc_translator.ts @@ -221,11 +221,12 @@ export class RPCTranslator { } // eslint-disable-next-line camelcase - aztec_txe_setTaggingSecretStrategy(...inputs: ForeignCallArgs) { + aztec_txe_setTaggingSecretStrategies(...inputs: ForeignCallArgs) { return callTxeHandler({ - oracle: 'aztec_txe_setTaggingSecretStrategy', + oracle: 'aztec_txe_setTaggingSecretStrategies', inputs, - handler: ([strategy]) => this.handlerAsTxe().setTaggingSecretStrategy(strategy), + handler: ([unconstrainedStrategy, constrainedStrategy]) => + this.handlerAsTxe().setTaggingSecretStrategies(unconstrainedStrategy, constrainedStrategy), }); } diff --git a/yarn-project/txe/src/txe_session.ts b/yarn-project/txe/src/txe_session.ts index b72b46b2b346..f571dfae57f1 100644 --- a/yarn-project/txe/src/txe_session.ts +++ b/yarn-project/txe/src/txe_session.ts @@ -20,7 +20,6 @@ import { RecipientTaggingStore, SenderTaggingStore, TaggingSecretSourcesStore, - type TaggingSecretStrategy, composeHooks, } from '@aztec/pxe/server'; import { @@ -58,6 +57,10 @@ import { z } from 'zod'; import { DEFAULT_ADDRESS, MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY, MAX_OFFCHAIN_EFFECT_LEN } from './constants.js'; import type { IAvmExecutionOracle, ITxeExecutionOracle } from './oracle/interfaces.js'; +import { + type TXETaggingSecretStrategies, + makeResolveTaggingSecretStrategyHook, +} from './oracle/tagging_secret_strategy.js'; import { TXEOraclePublicContext } from './oracle/txe_oracle_public_context.js'; import { callTxeLegacyHandler } from './oracle/txe_oracle_registry.js'; import { TXEOracleTopLevelContext } from './oracle/txe_oracle_top_level_context.js'; @@ -237,7 +240,7 @@ function emptyLastCallState(): LastCallState { export class TXESession implements TXESessionStateHandler { private state: SessionState = { name: 'TOP_LEVEL' }; private authwits: Map = new Map(); - private taggingSecretStrategy: TaggingSecretStrategy | undefined = undefined; + private taggingSecretStrategies: TXETaggingSecretStrategies = new Map(); private lastCallInfo: LastCallState = emptyLastCallState(); private txeOracleVersion: { major: number; minor: number } | undefined; @@ -357,7 +360,7 @@ export class TXESession implements TXESessionStateHandler { version, chainId, new Map(), - undefined, + new Map(), artifactResolver, rootPath, packageName, @@ -687,7 +690,7 @@ export class TXESession implements TXESessionStateHandler { this.version, this.chainId, this.authwits, - this.taggingSecretStrategy, + this.taggingSecretStrategies, this.artifactResolver, this.rootPath, this.packageName, @@ -737,7 +740,6 @@ export class TXESession implements TXESessionStateHandler { this.stateMachine.contractClassService, anchorBlock!, ); - const taggingSecretStrategy = this.taggingSecretStrategy; this.oracleHandler = new TXEPrivateExecutionOracle({ argsHash: Fr.ZERO, txContext: new TxContext(this.chainId, this.version, gasSettings), @@ -767,7 +769,7 @@ export class TXESession implements TXESessionStateHandler { txResolver: this.stateMachine.txResolver, simulator: new WASMSimulator(), hooks: composeHooks({ - resolveTaggingSecretStrategy: taggingSecretStrategy ? () => Promise.resolve(taggingSecretStrategy) : undefined, + resolveTaggingSecretStrategy: makeResolveTaggingSecretStrategyHook(this.taggingSecretStrategies), }), transientArrayService, }); @@ -890,13 +892,13 @@ export class TXESession implements TXESessionStateHandler { // accounts to PXE (via `addAccount`), etc. This is a slight inconsistency in the working model of this class, but // is not too bad. The `close` call below therefore only hands back the session-scoped values that a test // sets directly at the top level, outside any contract execution (e.g. via `advanceTimestampBy`, - // `addAuthWitness`, `setTaggingSecretStrategy`). The oracle handler is discarded on every state transition, + // `addAuthWitness`, `setTaggingSecretStrategies`). The oracle handler is discarded on every state transition, // so the session must seed these values into the contexts it creates later. // TODO: persisting authwits this way is quite unfortunate: they create a temporary utility context that would // otherwise reset them, so we'd not be able to pass more than one per execution. Ideally authwits would be passed // alongside a contract call instead of pre-seeded. - [this.nextBlockTimestamp, this.authwits, this.taggingSecretStrategy] = ( + [this.nextBlockTimestamp, this.authwits, this.taggingSecretStrategies] = ( this.oracleHandler as TXEOracleTopLevelContext ).close(); }