diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/resolved_tagging_strategy.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/resolved_tagging_strategy.nr index 6c36a5348b1a..e6af56261a6c 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/resolved_tagging_strategy.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/resolved_tagging_strategy.nr @@ -1,4 +1,5 @@ -use crate::protocol::{traits::{Deserialize, Serialize}, utils::reader::Reader}; +use crate::context::PrivateContext; +use crate::protocol::{address::AztecAddress, traits::{Deserialize, Serialize}, utils::reader::Reader}; use super::tag_secret_source::TagSecretSource; global NON_INTERACTIVE_HANDSHAKE: u8 = 1; @@ -27,6 +28,20 @@ impl ResolvedTaggingStrategy { self.kind == UNCONSTRAINED_SECRET } + /// Resolves this strategy into the [`TagSecretSource`] backing the message tag. + pub(crate) fn into_tag_secret_source( + self, + context: &mut PrivateContext, + sender: AztecAddress, + recipient: AztecAddress, + ) -> TagSecretSource { + if self.kind == NON_INTERACTIVE_HANDSHAKE { + TagSecretSource::new_non_interactive_handshake(context, sender, recipient) + } else { + TagSecretSource::unconstrained_secret(self.secret) + } + } + /// Validates a raw discriminant, as deserialization must always reject unknown values. fn from_parts(kind: u8, secret: Field) -> Self { let resolved = Self { kind, secret }; @@ -52,16 +67,6 @@ impl Deserialize for ResolvedTaggingStrategy { } } -impl From for TagSecretSource { - fn from(strategy: ResolvedTaggingStrategy) -> Self { - if strategy.kind == NON_INTERACTIVE_HANDSHAKE { - TagSecretSource::new_non_interactive_handshake() - } else { - TagSecretSource::unconstrained_secret(strategy.secret) - } - } -} - mod test { use crate::protocol::traits::{Deserialize, Serialize}; use super::ResolvedTaggingStrategy; 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 1a22bc46d4e0..056e5479782e 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr @@ -4,7 +4,6 @@ use crate::context::PrivateContext; use crate::messages::delivery::{ - constrained_delivery::emit_sequence_nullifier, OnchainDeliveryMode, tag_derivation::{existing_handshake_secrets_or_else, TagDerivation}, tag_secret_source::TagSecretSource, @@ -28,15 +27,17 @@ pub(crate) fn derive_log_tag( recipient: AztecAddress, tag_derivation: Option, ) -> Field { - let source = tag_derivation.map_or_else(|| default_tag_secret_source(sender, recipient, mode), |derivation| { - derivation.into_tag_secret_source(sender, recipient) - }); + let source = tag_derivation.map_or_else( + || default_tag_secret_source(context, sender, recipient, mode), + |derivation| derivation.into_tag_secret_source(context, sender, recipient), + ); tag_with(source, context, mode, sender, recipient) } /// The wallet-resolved default: reuse a handshake already registered for the pair, otherwise let the wallet resolve /// the tagging secret strategy. fn default_tag_secret_source( + context: &mut PrivateContext, sender: AztecAddress, recipient: AztecAddress, mode: OnchainDeliveryMode, @@ -49,7 +50,7 @@ fn default_tag_secret_source( // secret before a constrained tag is emitted, so an untrusted strategy can't produce a valid unbacked // constrained tag. let strategy = unsafe { resolve_tagging_strategy(sender, recipient, mode) }; - strategy.into() + strategy.into_tag_secret_source(context, sender, recipient) }, ) } @@ -61,19 +62,18 @@ fn tag_with( sender: AztecAddress, recipient: AztecAddress, ) -> Field { - let secrets = source.obtain_secrets(context, sender, recipient); + let secret = source.tag_secret(); // Safety: the index is untrusted. Constrained delivery constrains it below before emitting the tag; unconstrained // discovery tolerates gaps, so a wrong index only yields an undiscoverable tag. - let index = unsafe { get_next_tagging_index(secrets.shared, mode) }; + let index = unsafe { get_next_tagging_index(secret, mode) }; if mode == OnchainDeliveryMode::onchain_constrained() { - source.constrain_secrets(context, sender, recipient, secrets, index); - emit_sequence_nullifier(context, sender, recipient, secrets, index); + source.constrain_tag_secret(context, sender, recipient, index); } - // The discovery tag stays derived from the shared secret only, so the recipient still finds every message. - tag_from_secret_and_index(secrets.shared, index, mode) + // The discovery tag stays derived from the tag secret only, so the recipient still finds every message. + tag_from_secret_and_index(secret, index, mode) } fn tag_domain_separator(mode: OnchainDeliveryMode) -> u32 { diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_derivation.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_derivation.nr index c8b46f495c2a..d2090a2f3f45 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_derivation.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_derivation.nr @@ -1,3 +1,4 @@ +use crate::context::PrivateContext; use crate::messages::delivery::{ handshake::get_existing_app_siloed_handshake_secrets, tag_secret_source::TagSecretSource, }; @@ -39,13 +40,17 @@ impl TagDerivation { } /// Resolves this choice into the [`TagSecretSource`] backing the message tag. - pub(crate) fn into_tag_secret_source(self, sender: AztecAddress, recipient: AztecAddress) -> TagSecretSource { + pub(crate) fn into_tag_secret_source( + self, + context: &mut PrivateContext, + sender: AztecAddress, + recipient: AztecAddress, + ) -> TagSecretSource { if self.kind == NON_INTERACTIVE_HANDSHAKE { - // A fresh handshake is created in a constrained manner in `obtain_secrets`. existing_handshake_secrets_or_else( sender, recipient, - || TagSecretSource::new_non_interactive_handshake(), + || TagSecretSource::new_non_interactive_handshake(context, sender, recipient), ) } else { // Safety: the secret is untrusted, but address-derived tagging is unconstrained-only, where a wrong @@ -73,8 +78,11 @@ pub(crate) fn existing_handshake_secrets_or_else( } mod test { + use crate::context::PrivateContext; + use crate::hash::hash_args; use crate::messages::delivery::handshake::AppSiloedHandshakeSecrets; use crate::messages::delivery::tag_secret_source::TagSecretSource; + use crate::oracle::execution_cache; use crate::protocol::{address::AztecAddress, traits::{FromField, Serialize}}; use crate::test::helpers::test_environment::TestEnvironment; use super::TagDerivation; @@ -88,10 +96,10 @@ mod test { let env = TestEnvironment::new(); let secret: Field = 7; - env.private_context(|_context| { + env.private_context(|context| { let _ = OracleMock::mock("aztec_prv_getAppTaggingSecret").returns(Option::some(secret)); - let source = TagDerivation::address_derived().into_tag_secret_source(SENDER, RECIPIENT); + let source = TagDerivation::address_derived().into_tag_secret_source(context, SENDER, RECIPIENT); assert_eq(source, TagSecretSource::unconstrained_secret(secret)); }); } @@ -101,11 +109,11 @@ mod test { let env = TestEnvironment::new(); let random_secret: Field = 999; - env.private_context(|_context| { + env.private_context(|context| { let _ = OracleMock::mock("aztec_prv_getAppTaggingSecret").returns(Option::::none()); let _ = OracleMock::mock("aztec_misc_getRandomField").returns(random_secret); - let source = TagDerivation::address_derived().into_tag_secret_source(SENDER, RECIPIENT); + let source = TagDerivation::address_derived().into_tag_secret_source(context, SENDER, RECIPIENT); assert_eq(source, TagSecretSource::unconstrained_secret(random_secret)); }); } @@ -115,10 +123,10 @@ mod test { let env = TestEnvironment::new(); let secrets = AppSiloedHandshakeSecrets { shared: 7, sender_only: 99 }; - env.private_context(|_context| { + env.private_context(|context| { mock_existing_handshake_secrets(Option::some(secrets)); - let source = TagDerivation::non_interactive_handshake().into_tag_secret_source(SENDER, RECIPIENT); + let source = TagDerivation::non_interactive_handshake().into_tag_secret_source(context, SENDER, RECIPIENT); assert_eq(source, TagSecretSource::existing_handshake(secrets)); }); } @@ -126,16 +134,28 @@ mod test { #[test] unconstrained fn non_interactive_handshake_creates_a_fresh_one_when_none_exists() { let env = TestEnvironment::new(); + let secrets = AppSiloedHandshakeSecrets { shared: 7, sender_only: 99 }; - env.private_context(|_context| { + env.private_context(|context| { mock_existing_handshake_secrets(Option::none()); + mock_handshake_creation(context, secrets); - let source = TagDerivation::non_interactive_handshake().into_tag_secret_source(SENDER, RECIPIENT); - assert_eq(source, TagSecretSource::new_non_interactive_handshake()); + let source = TagDerivation::non_interactive_handshake().into_tag_secret_source(context, SENDER, RECIPIENT); + assert_eq(source.tag_secret(), secrets.shared); }); } unconstrained fn mock_existing_handshake_secrets(maybe_secrets: Option) { let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(maybe_secrets.serialize()); } + + // The registry's `non_interactive_handshake` call returns the app-siloed secrets via the returns cache. + unconstrained fn mock_handshake_creation(context: &mut PrivateContext, secrets: AppSiloedHandshakeSecrets) { + let returns = secrets.serialize(); + let returns_hash = hash_args(returns); + execution_cache::store(returns, returns_hash); + let child_call_end_counter = context.get_side_effect_counter() + 1; + let _ = + OracleMock::mock("aztec_prv_callPrivateFunction").returns((child_call_end_counter, returns_hash)).times(1); + } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_source.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_source.nr index e880dda6f011..5ea04faecfbb 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_source.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_source.nr @@ -1,6 +1,6 @@ use crate::context::PrivateContext; use crate::messages::delivery::{ - constrained_delivery::constrain_preexisting_handshake_secrets, + constrained_delivery::{constrain_preexisting_handshake_secrets, emit_sequence_nullifier}, handshake::{AppSiloedHandshakeSecrets, create_non_interactive_handshake}, }; use crate::protocol::address::AztecAddress; @@ -10,68 +10,60 @@ global EXISTING_HANDSHAKE: u8 = 1; global NEW_NON_INTERACTIVE_HANDSHAKE: u8 = 2; global UNCONSTRAINED_SECRET: u8 = 3; -/// How a message's tagging secrets are sourced and, for constrained delivery, constrained. +/// How a message's tag secret is sourced and, for constrained delivery, constrained. +#[derive(Eq)] pub(crate) struct TagSecretSource { kind: u8, - secrets: AppSiloedHandshakeSecrets, -} - -// Hand-written rather than `#[derive(Eq)]`: the derive resolves each field's `Eq` at expansion time and can't see -// `AppSiloedHandshakeSecrets`'s own derived `Eq` (a cross-module derive dependency), whereas this direct comparison -// resolves it normally. -impl Eq for TagSecretSource { - fn eq(self, other: Self) -> bool { - (self.kind == other.kind) & (self.secrets == other.secrets) - } + /// The secret the message tag derives from. Every source resolves to exactly one. + tag_secret: Field, + /// The handshake sender-only secret, folded into the constrained-delivery sequence nullifier. Only meaningful + /// for handshake-backed sources. + sender_only: Field, } impl TagSecretSource { /// Reuses the secrets of a handshake already registered for the pair (mode-agnostic). pub(crate) fn existing_handshake(secrets: AppSiloedHandshakeSecrets) -> Self { - Self { kind: EXISTING_HANDSHAKE, secrets } + Self { kind: EXISTING_HANDSHAKE, tag_secret: secrets.shared, sender_only: secrets.sender_only } } /// Establishes a fresh non-interactive handshake, publishing information about the recipient onchain. - pub(crate) fn new_non_interactive_handshake() -> Self { - Self { kind: NEW_NON_INTERACTIVE_HANDSHAKE, secrets: AppSiloedHandshakeSecrets { shared: 0, sender_only: 0 } } + pub(crate) fn new_non_interactive_handshake( + context: &mut PrivateContext, + sender: AztecAddress, + recipient: AztecAddress, + ) -> Self { + let secrets = create_non_interactive_handshake( + context, + STANDARD_HANDSHAKE_REGISTRY_ADDRESS, + sender, + recipient, + ); + Self { kind: NEW_NON_INTERACTIVE_HANDSHAKE, tag_secret: secrets.shared, sender_only: secrets.sender_only } } - /// A ready-to-use unconstrained secret the wallet resolved. Sound only for unconstrained delivery, which derives - /// the discovery tag from the shared secret alone and never folds in a sender-only secret. + /// A ready-to-use unconstrained secret the wallet resolved. Sound only for unconstrained delivery, which never + /// anchors a sequence, so no sender-only secret is involved. pub(crate) fn unconstrained_secret(secret: Field) -> Self { - Self { kind: UNCONSTRAINED_SECRET, secrets: AppSiloedHandshakeSecrets { shared: secret, sender_only: 0 } } + Self { kind: UNCONSTRAINED_SECRET, tag_secret: secret, sender_only: 0 } } - /// Returns the app-siloed handshake secrets, performing any handshake creation the source requires. - pub(crate) fn obtain_secrets( - self, - context: &mut PrivateContext, - sender: AztecAddress, - recipient: AztecAddress, - ) -> AppSiloedHandshakeSecrets { - if self.kind == NEW_NON_INTERACTIVE_HANDSHAKE { - create_non_interactive_handshake( - context, - STANDARD_HANDSHAKE_REGISTRY_ADDRESS, - sender, - recipient, - ) - } else { - // An existing or unconstrained secret is already resolved; only a fresh handshake must be created. - self.secrets - } + /// The secret the message tag derives from. + pub(crate) fn tag_secret(self) -> Field { + self.tag_secret } - /// Constrains `(secrets, index)` for constrained delivery, each source proving its secrets its own way. A source - /// that cannot back constrained delivery rejects it here. Only called for constrained delivery. - pub(crate) fn constrain_secrets( + /// Constrains `(tag secret, index)` for constrained delivery and emits the sequence nullifier, each source + /// proving its secrets its own way. A source that cannot back constrained delivery rejects it here. Only called + /// for constrained delivery. + pub(crate) fn constrain_tag_secret( self, context: &mut PrivateContext, sender: AztecAddress, recipient: AztecAddress, - secrets: AppSiloedHandshakeSecrets, index: u32, ) { + let secrets = AppSiloedHandshakeSecrets::from(self); if self.kind == EXISTING_HANDSHAKE { constrain_preexisting_handshake_secrets( context, @@ -89,11 +81,22 @@ impl TagSecretSource { "an unconstrained tagging secret cannot back constrained delivery", ); } + emit_sequence_nullifier(context, sender, recipient, secrets, index); + } +} + +/// The handshake-secrets view of a source. For a source that is not handshake-backed the sender-only secret is a +/// filler zero, which constrained delivery (the only consumer of it) rejects. +impl From for AppSiloedHandshakeSecrets { + fn from(source: TagSecretSource) -> Self { + Self { shared: source.tag_secret, sender_only: source.sender_only } } } mod test { + use crate::context::PrivateContext; use crate::hash::hash_args; + use crate::messages::delivery::constrained_delivery::compute_constrained_msg_nullifier; use crate::messages::delivery::handshake::AppSiloedHandshakeSecrets; use crate::oracle::execution_cache; use crate::protocol::{address::AztecAddress, traits::{FromField, Serialize}}; @@ -105,44 +108,40 @@ mod test { global RECIPIENT: AztecAddress = AztecAddress::from_field(8); #[test] - unconstrained fn existing_handshake_obtains_its_secrets() { + unconstrained fn existing_handshake_holds_and_constrains_its_secrets() { let env = TestEnvironment::new(); let secrets = AppSiloedHandshakeSecrets { shared: 42, sender_only: 7 }; + let index: u32 = 3; + env.private_context(|context| { - assert_eq( - TagSecretSource::existing_handshake(secrets).obtain_secrets(context, SENDER, RECIPIENT), - secrets, - ); + let _ = OracleMock::mock("aztec_prv_isNullifierPending").returns(false).times(1); + + let source = TagSecretSource::existing_handshake(secrets); + assert_eq(source.tag_secret(), secrets.shared); + + source.constrain_tag_secret(context, SENDER, RECIPIENT, index); + assert_emitted_sequence_nullifier(context, secrets, index); }); } #[test] - unconstrained fn unconstrained_secret_obtains_its_secret() { - let env = TestEnvironment::new(); - env.private_context(|context| { - assert_eq(TagSecretSource::unconstrained_secret(42).obtain_secrets(context, SENDER, RECIPIENT).shared, 42); - }); + unconstrained fn unconstrained_secret_holds_its_secret() { + assert_eq(TagSecretSource::unconstrained_secret(42).tag_secret(), 42); } #[test] - unconstrained fn new_non_interactive_handshake_obtains_the_bootstrapped_secrets() { + unconstrained fn new_non_interactive_handshake_creates_and_constrains_the_bootstrapped_secrets() { let env = TestEnvironment::new(); let secrets = AppSiloedHandshakeSecrets { shared: 7, sender_only: 9 }; env.private_context(|context| { - // The registry's `non_interactive_handshake` call returns the app-siloed secrets via the returns cache. - let returns = secrets.serialize(); - let returns_hash = hash_args(returns); - execution_cache::store(returns, returns_hash); - let child_call_end_counter = context.get_side_effect_counter() + 1; - let _ = OracleMock::mock("aztec_prv_callPrivateFunction") - .returns((child_call_end_counter, returns_hash)) - .times(1); - - assert_eq( - TagSecretSource::new_non_interactive_handshake().obtain_secrets(context, SENDER, RECIPIENT), - secrets, - ); + mock_handshake_creation(context, secrets); + + let source = TagSecretSource::new_non_interactive_handshake(context, SENDER, RECIPIENT); + assert_eq(source.tag_secret(), secrets.shared); + + source.constrain_tag_secret(context, SENDER, RECIPIENT, 0); + assert_emitted_sequence_nullifier(context, secrets, 0); }); } @@ -151,11 +150,15 @@ mod test { unconstrained fn fresh_handshake_secret_must_start_at_index_zero() { let env = TestEnvironment::new(); env.private_context(|context| { - TagSecretSource::new_non_interactive_handshake().constrain_secrets( + mock_handshake_creation( context, - AztecAddress::zero(), - AztecAddress::zero(), AppSiloedHandshakeSecrets { shared: 0, sender_only: 0 }, + ); + + TagSecretSource::new_non_interactive_handshake(context, SENDER, RECIPIENT).constrain_tag_secret( + context, + SENDER, + RECIPIENT, 1, ); }); @@ -165,13 +168,34 @@ mod test { unconstrained fn unconstrained_secret_cannot_back_constrained_delivery() { let env = TestEnvironment::new(); env.private_context(|context| { - TagSecretSource::unconstrained_secret(7).constrain_secrets( + TagSecretSource::unconstrained_secret(7).constrain_tag_secret( context, AztecAddress::zero(), AztecAddress::zero(), - AppSiloedHandshakeSecrets { shared: 7, sender_only: 0 }, 0, ); }); } + + unconstrained fn assert_emitted_sequence_nullifier( + context: &mut PrivateContext, + secrets: AppSiloedHandshakeSecrets, + index: u32, + ) { + assert_eq(context.nullifiers.len(), 1); + assert_eq( + context.nullifiers.get(0).inner.value, + compute_constrained_msg_nullifier(SENDER, RECIPIENT, secrets, index), + ); + } + + // The registry's `non_interactive_handshake` call returns the app-siloed secrets via the returns cache. + unconstrained fn mock_handshake_creation(context: &mut PrivateContext, secrets: AppSiloedHandshakeSecrets) { + let returns = secrets.serialize(); + let returns_hash = hash_args(returns); + execution_cache::store(returns, returns_hash); + let child_call_end_counter = context.get_side_effect_counter() + 1; + let _ = + OracleMock::mock("aztec_prv_callPrivateFunction").returns((child_call_end_counter, returns_hash)).times(1); + } }