diff --git a/docs/docs-developers/docs/aztec-nr/framework-description/note_delivery.md b/docs/docs-developers/docs/aztec-nr/framework-description/note_delivery.md index 43c180db92ff..f4f94fbce7ee 100644 --- a/docs/docs-developers/docs/aztec-nr/framework-description/note_delivery.md +++ b/docs/docs-developers/docs/aztec-nr/framework-description/note_delivery.md @@ -161,7 +161,11 @@ Ask yourself: **"Is the sender incentivized to deliver this note correctly?"** ## Tagging secret strategy -Onchain delivery tags every message so the recipient can find it efficiently (see [note discovery](#note-discovery-and-the-sender) below). Computing a tag requires a secret shared between sender and recipient, and there is more than one way for the two parties to come to share it. When an onchain handshake has been registered for the pair, the secret derived from it is reused directly. Otherwise the wallet decides how to proceed, since it knows which secrets it holds and how it wants to reach the recipient. +Onchain delivery tags every message so the recipient can find it efficiently (see [note discovery](#note-discovery-and-the-sender) below). Computing a tag requires a secret shared between sender and recipient, and there is more than one way for the two parties to come to share it. The derivation is decided in this order: + +1. A contract can fix it itself at the point of delivery (see [overriding the strategy from the contract](#overriding-the-strategy-from-the-contract)). +2. Otherwise, when an onchain handshake has been registered for the pair, the secret derived from it is reused directly. +3. Otherwise, the wallet decides how to proceed, since it knows which secrets it holds and how it wants to reach the recipient. The wallet's answer is a **tagging secret strategy**: it expresses *which* secret to use, and if necessary, PXE performs a [Diffie-Hellman key exchange](https://www.geeksforgeeks.org/computer-networks/diffie-hellman-key-exchange-and-perfect-forward-secrecy/) and/or app-siloing before handing the ready-to-use secret to the contract. Wallets therefore never reimplement that derivation. There are three strategies today: @@ -186,6 +190,16 @@ When no `resolveTaggingSecretStrategy` hook is configured, the PXE applies a def Wallets provide the strategy through the `resolveTaggingSecretStrategy` [execution hook](../../foundational-topics/pxe/execution_hooks.md) when creating their PXE. The hook receives the message context (executing contract, sender, recipient and delivery mode), so a wallet can answer per message instead of with a fixed value. That page also covers how to configure a strategy in Noir tests. +### Overriding the strategy from the contract + +A contract can fix the derivation at the point of delivery with the builder's `via_*` methods. When it does, the wallet is not consulted at all; otherwise the wallet resolves the strategy as usual: + +```rust +MessageDelivery::onchain_unconstrained().via_address_derived_secret() +``` + +Unconstrained delivery exposes `via_non_interactive_handshake()` and `via_address_derived_secret()`. Constrained delivery exposes only `via_non_interactive_handshake()`, since an address-derived secret cannot back constrained delivery. + ## Note Discovery and the Sender When a note is delivered, recipients need to discover it among all the encrypted logs on the network. Aztec.nr uses a **tagging system** that requires computing a shared secret between the sender and recipient. @@ -194,7 +208,7 @@ When a note is delivered, recipients need to discover it among all the encrypted The "sender" for note discovery is **not the contract calling `.deliver()`**. Instead, it's the **account contract** that initiated the transaction. -When your wallet submits a transaction, it tells PXE which address to use as the sender for tags (typically the originating account). Recipients compute the tag to find their notes from a secret shared between the sender and recipient, and there is [more than one way to establish that secret](#tagging-secret-strategy), chosen by the wallet. Contracts can override the sender at message delivery via the `with_sender` builder method, which works for both constrained and unconstrained delivery, e.g. `MessageDelivery::onchain_constrained().with_sender(address)`. +When your wallet submits a transaction, it tells PXE which address to use as the sender for tags (typically the originating account). Recipients compute the tag to find their notes from a secret shared between the sender and recipient, and there is [more than one way to establish that secret](#tagging-secret-strategy), chosen by the wallet. Contracts can override the sender at message delivery via the `with_sender` builder method, which works for both constrained and unconstrained delivery, e.g. `MessageDelivery::onchain_constrained().with_sender(address)`. They can similarly override how the tag secret is derived via the builder's `via_*` methods; see [overriding the strategy from the contract](#overriding-the-strategy-from-the-contract). **Example:** If Alice uses her account contract to call a token contract that mints tokens to Bob, the "sender for tags" is Alice's account contract address, not the token contract address. diff --git a/docs/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md b/docs/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md index 0a987cfeb26b..d726f9ee10b3 100644 --- a/docs/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md +++ b/docs/docs-developers/docs/foundational-topics/advanced/storage/note_discovery.md @@ -25,7 +25,7 @@ In Aztec, each emitted log is an array of fields, e.g. `[tag, x, y, z]`. The fir Every tag is derived the same way: `poseidon2(secret, index)`. -What varies is how the sender and recipient come to share `secret`. This is the [tagging secret strategy](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy), chosen by the wallet. +What varies is how the sender and recipient come to share `secret`. This is the [tagging secret strategy](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy), chosen by the wallet by default, though a contract can [override it at delivery](../../../aztec-nr/framework-description/note_delivery.md#overriding-the-strategy-from-the-contract). There are three strategies, described below. They differ only in how `secret` is established. Once `secret` exists, the tag is derived from it the same way for all three. @@ -110,7 +110,7 @@ There are three broad families of solutions to this problem: **b) Tagging with known sender** - You know who will send you messages and search for those specifically. This is very fast and allows you to remove senders who spam you. However, it cannot be constrained, i.e., it cannot guarantee that the recipient will find the message. It also requires registering each sender's address in advance with `wallet.registerSender(address)`, so you must learn that address first. -**c) Tagging with a handshake** - The sender and recipient execute a handshake to agree on a tagging secret, after which regular tagging works, so the recipient can discover messages without having registered the sender in advance. A handshake can be interactive (the two coordinate offchain) or non-interactive (published onchain, which needs no prior coordination but reveals a sender has done a handshake with the recipient). The wallet is the one that determines the type of handshake to use (see [tagging secret strategy](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy)). +**c) Tagging with a handshake** - The sender and recipient execute a handshake to agree on a tagging secret, after which regular tagging works, so the recipient can discover messages without having registered the sender in advance. A handshake can be interactive (the two coordinate offchain) or non-interactive (published onchain, which needs no prior coordination but reveals a sender has done a handshake with the recipient). By default the wallet determines the type of handshake to use, though a contract can override the choice at delivery (see [tagging secret strategy](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy)). See the [Note Delivery](../../../aztec-nr/framework-description/note_delivery.md) documentation for more details on how the sender is used when delivering notes. diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr index 8d6de86289b2..efd92c1019c6 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr @@ -1,5 +1,6 @@ use crate::protocol::address::AztecAddress; use super::mode::{DeliveryMode, OnchainDeliveryMode}; +use super::tag_derivation::TagDerivation; /// Specifies how to deliver a message to a recipient. /// @@ -22,6 +23,7 @@ use super::mode::{DeliveryMode, OnchainDeliveryMode}; pub struct MessageDelivery { mode: DeliveryMode, sender_override: Option, + tag_derivation: Option, } impl MessageDelivery { @@ -33,6 +35,10 @@ impl MessageDelivery { self.sender_override } + pub(crate) fn tag_derivation(self) -> Option { + self.tag_derivation + } + /// Delivers the message fully off-chain, with no guarantees whatsoever. /// /// ## Use Cases @@ -134,8 +140,8 @@ impl MessageDelivery { /// /// Delivering the message does produce on-chain information in the form of private logs, so transactions that /// deliver many messages this way might be identifiable by the large number of logs. - pub fn onchain_unconstrained() -> OnchainDelivery { - OnchainDelivery::new(OnchainDeliveryMode::onchain_unconstrained()) + pub fn onchain_unconstrained() -> OnchainUnconstrainedDelivery { + OnchainUnconstrainedDelivery::new() } /// Delivers the message on-chain, guaranteeing the recipient will receive the correct content. @@ -185,8 +191,8 @@ impl MessageDelivery { /// /// Delivering the message does produce on-chain information in the form of private logs and nullifiers, so /// transactions that deliver many messages this way might be identifiable by these markers. - pub fn onchain_constrained() -> OnchainDelivery { - OnchainDelivery::new(OnchainDeliveryMode::onchain_constrained()) + pub fn onchain_constrained() -> OnchainConstrainedDelivery { + OnchainConstrainedDelivery::new() } } @@ -201,20 +207,28 @@ pub struct OffchainDelivery {} impl MessageDeliveryBuilder for OffchainDelivery { fn build_message_delivery(self) -> MessageDelivery { - MessageDelivery { mode: DeliveryMode::offchain(), sender_override: Option::none() } + MessageDelivery { + mode: DeliveryMode::offchain(), + sender_override: Option::none(), + tag_derivation: Option::none(), + } } } -/// On-chain delivery. Returned by both [`MessageDelivery::onchain_unconstrained`] and -/// [`MessageDelivery::onchain_constrained`], which differ only in the [`OnchainDeliveryMode`] they carry. -pub struct OnchainDelivery { - mode: OnchainDeliveryMode, +/// On-chain unconstrained delivery. Returned by [`MessageDelivery::onchain_unconstrained`]. +/// +/// By default the tag reuses a handshake already registered for the pair, and +/// otherwise falls back to the wallet-resolved [tagging secret +/// strategy][`crate::messages::delivery::ResolvedTaggingStrategy`]. The contract can also fix the +/// derivation, and its choice is honored over this default. +pub struct OnchainUnconstrainedDelivery { sender_override: Option, + tag_derivation: Option, } -impl OnchainDelivery { - fn new(mode: OnchainDeliveryMode) -> Self { - Self { mode, sender_override: Option::none() } +impl OnchainUnconstrainedDelivery { + fn new() -> Self { + Self { sender_override: Option::none(), tag_derivation: Option::none() } } /// Overrides the sender address used for discovery tag derivation. @@ -229,30 +243,109 @@ impl OnchainDelivery { /// ## Examples /// /// ```noir + /// MessageDelivery::onchain_unconstrained().with_sender(self.address) + /// ``` + pub fn with_sender(&mut self, sender: AztecAddress) -> Self { + self.sender_override = Option::some(sender); + *self + } + + /// Derives the discovery tag from a non-interactive handshake for the pair, reusing an existing one and creating a + /// fresh one only when none exists. + pub fn via_non_interactive_handshake(&mut self) -> Self { + self.tag_derivation = Option::some(TagDerivation::non_interactive_handshake()); + *self + } + + /// Derives the discovery tag from the address-derived secret for the `(sender, recipient)` pair, established via + /// Diffie-Hellman between their addresses. Leaves no on-chain trace and never consults the handshake registry. + /// + /// ## Examples + /// + /// ```noir + /// MessageDelivery::onchain_unconstrained().via_address_derived_secret() + /// ``` + pub fn via_address_derived_secret(&mut self) -> Self { + self.tag_derivation = Option::some(TagDerivation::address_derived()); + *self + } +} + +impl MessageDeliveryBuilder for OnchainUnconstrainedDelivery { + fn build_message_delivery(self) -> MessageDelivery { + MessageDelivery { + mode: DeliveryMode::onchain_unconstrained(), + sender_override: self.sender_override, + tag_derivation: self.tag_derivation, + } + } +} + +impl From for OnchainDeliveryMode { + fn from(_delivery: OnchainUnconstrainedDelivery) -> OnchainDeliveryMode { + OnchainDeliveryMode::onchain_unconstrained() + } +} + +/// On-chain constrained delivery. Returned by [`MessageDelivery::onchain_constrained`]. +/// +/// By default the tag reuses a handshake already registered for the pair, and otherwise falls back to the +/// wallet-resolved [tagging secret strategy][`crate::messages::delivery::ResolvedTaggingStrategy`]. Constrained +/// delivery only supports handshake-backed derivations. +pub struct OnchainConstrainedDelivery { + sender_override: Option, + tag_derivation: Option, +} + +impl OnchainConstrainedDelivery { + fn new() -> Self { + Self { sender_override: Option::none(), tag_derivation: Option::none() } + } + + /// Overrides the sender address used for discovery tag derivation. + /// + /// See [`OnchainUnconstrainedDelivery::with_sender`] for details. + /// + /// ## Examples + /// + /// ```noir /// MessageDelivery::onchain_constrained().with_sender(self.address) /// ``` pub fn with_sender(&mut self, sender: AztecAddress) -> Self { self.sender_override = Option::some(sender); *self } + + /// Derives the discovery tag from a non-interactive handshake for the pair, reusing an existing one and creating a + /// fresh one only when none exists. Overrides the wallet's default resolution. + /// + /// Constrained delivery only supports constrained secrets (e.g., handshake-registry backed derivations) + pub fn via_non_interactive_handshake(&mut self) -> Self { + self.tag_derivation = Option::some(TagDerivation::non_interactive_handshake()); + *self + } } -impl MessageDeliveryBuilder for OnchainDelivery { +impl MessageDeliveryBuilder for OnchainConstrainedDelivery { fn build_message_delivery(self) -> MessageDelivery { - MessageDelivery { mode: self.mode.into(), sender_override: self.sender_override } + MessageDelivery { + mode: DeliveryMode::onchain_constrained(), + sender_override: self.sender_override, + tag_derivation: self.tag_derivation, + } } } -impl From for OnchainDeliveryMode { - fn from(delivery: OnchainDelivery) -> OnchainDeliveryMode { - delivery.mode +impl From for OnchainDeliveryMode { + fn from(_delivery: OnchainConstrainedDelivery) -> OnchainDeliveryMode { + OnchainDeliveryMode::onchain_constrained() } } mod test { use crate::protocol::address::AztecAddress; use crate::protocol::traits::FromField; - use super::{DeliveryMode, MessageDelivery, MessageDeliveryBuilder, OnchainDeliveryMode}; + use super::{DeliveryMode, MessageDelivery, MessageDeliveryBuilder, OnchainDeliveryMode, TagDerivation}; #[test] fn onchain_deliveries_default_to_no_sender() { @@ -296,4 +389,18 @@ mod test { == DeliveryMode::onchain_constrained(), ); } + + #[test] + fn tag_derivation_defaults_to_none_and_via_methods_populate_it() { + assert(MessageDelivery::onchain_unconstrained().build_message_delivery().tag_derivation().is_none()); + assert(MessageDelivery::onchain_constrained().build_message_delivery().tag_derivation().is_none()); + + let unconstrained_delivery = + MessageDelivery::onchain_unconstrained().via_address_derived_secret().build_message_delivery(); + assert_eq(unconstrained_delivery.tag_derivation(), Option::some(TagDerivation::address_derived())); + + let constrained_delivery = + MessageDelivery::onchain_constrained().via_non_interactive_handshake().build_message_delivery(); + assert_eq(constrained_delivery.tag_derivation(), Option::some(TagDerivation::non_interactive_handshake())); + } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr index 169394fc12f8..a8810514a96a 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr @@ -2,6 +2,7 @@ mod builder; mod mode; mod resolved_tagging_strategy; mod tag; +pub(crate) mod tag_derivation; pub(crate) mod tag_secret_source; pub mod constrained_delivery; @@ -19,8 +20,11 @@ use crate::{ use crate::protocol::address::AztecAddress; use mode::DeliveryMode; use tag::derive_log_tag; +use tag_derivation::TagDerivation; -pub use builder::{MessageDelivery, MessageDeliveryBuilder, OffchainDelivery, OnchainDelivery}; +pub use builder::{ + MessageDelivery, MessageDeliveryBuilder, OffchainDelivery, OnchainConstrainedDelivery, OnchainUnconstrainedDelivery, +}; pub use mode::OnchainDeliveryMode; pub use resolved_tagging_strategy::ResolvedTaggingStrategy; @@ -63,6 +67,7 @@ where assert_constant(deliver_as_offchain_message); let sender_override = delivery.sender_override(); + let tag_derivation = delivery.tag_derivation(); if deliver_as_offchain_message { let contract_address = context.this_address(); @@ -80,6 +85,7 @@ where recipient, mode, sender_override, + tag_derivation, ); } } @@ -91,10 +97,18 @@ fn do_onchain_private_message_delivery( recipient: AztecAddress, mode: DeliveryMode, sender_override: Option, + tag_derivation: Option, ) { let is_constrained = mode == DeliveryMode::onchain_constrained(); assert_constant(is_constrained); + // The tag derivation, both whether one is set and which variant it is, must be a compile-time constant so unused + // derivations are eliminated. + assert_constant(tag_derivation.is_some()); + if tag_derivation.is_some() { + tag_derivation.unwrap_unchecked().assert_kind_is_constant(); + } + let onchain_mode = to_onchain_delivery_mode(mode); let sender = resolve_sender(sender_override); @@ -105,7 +119,7 @@ fn do_onchain_private_message_delivery( || AES128::encrypt(encode_into_message_plaintext(), recipient, contract_address), ); - let log_tag = derive_log_tag(context, onchain_mode, sender, recipient); + let log_tag = derive_log_tag(context, onchain_mode, sender, recipient, tag_derivation); // This value must be constant to avoid predicating the context calls below, which might result in // the context's arrays having unknown compile time write indices and hence dramatically increasing constraints 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 d77bf8420232..1a22bc46d4e0 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr @@ -4,8 +4,10 @@ use crate::context::PrivateContext; use crate::messages::delivery::{ - constrained_delivery::emit_sequence_nullifier, handshake::get_existing_app_siloed_handshake_secrets, - OnchainDeliveryMode, tag_secret_source::TagSecretSource, + constrained_delivery::emit_sequence_nullifier, + OnchainDeliveryMode, + tag_derivation::{existing_handshake_secrets_or_else, TagDerivation}, + tag_secret_source::TagSecretSource, }; use crate::oracle::notes::get_next_tagging_index; use crate::oracle::resolve_tagging_strategy::resolve_tagging_strategy; @@ -14,21 +16,34 @@ use crate::protocol::{ constants::{DOM_SEP__CONSTRAINED_MSG_LOG_TAG, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG}, hash::{compute_log_tag, poseidon2_hash}, }; -use crate::standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS; /// Derives the discovery log tag for an on-chain delivery. +/// +/// `tag_derivation` is the contract's compile-time choice of tag secret source, or `None` for the wallet-resolved +/// default. It must be a compile-time constant so unused derivations are eliminated. pub(crate) fn derive_log_tag( context: &mut PrivateContext, mode: OnchainDeliveryMode, sender: AztecAddress, recipient: AztecAddress, + tag_derivation: Option, ) -> Field { - // Safety: this only selects which source backs the tag. An existing handshake's secrets are constrained against the - // registry before a constrained tag is emitted; otherwise the wallet's resolved source runs. - let existing = - unsafe { get_existing_app_siloed_handshake_secrets(STANDARD_HANDSHAKE_REGISTRY_ADDRESS, sender, recipient) }; + let source = tag_derivation.map_or_else(|| default_tag_secret_source(sender, recipient, mode), |derivation| { + derivation.into_tag_secret_source(sender, recipient) + }); + tag_with(source, context, mode, sender, recipient) +} - let source: TagSecretSource = existing.map_or_else( +/// 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( + sender: AztecAddress, + recipient: AztecAddress, + mode: OnchainDeliveryMode, +) -> TagSecretSource { + existing_handshake_secrets_or_else( + sender, + recipient, || { // Safety: the strategy only selects which source runs; each source constrains (or rejects) its own // secret before a constrained tag is emitted, so an untrusted strategy can't produce a valid unbacked @@ -36,10 +51,7 @@ pub(crate) fn derive_log_tag( let strategy = unsafe { resolve_tagging_strategy(sender, recipient, mode) }; strategy.into() }, - |handshake_secrets| TagSecretSource::existing_handshake(handshake_secrets), - ); - - tag_with(source, context, mode, sender, recipient) + ) } fn tag_with( @@ -81,7 +93,7 @@ fn tag_from_secret_and_index(secret: Field, index: u32, mode: OnchainDeliveryMod mod test { use crate::messages::delivery::handshake::AppSiloedHandshakeSecrets; - use crate::messages::delivery::ResolvedTaggingStrategy; + use crate::messages::delivery::{ResolvedTaggingStrategy, tag_derivation::TagDerivation}; use crate::protocol::{ address::AztecAddress, constants::{DOM_SEP__CONSTRAINED_MSG_LOG_TAG, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG}, @@ -118,7 +130,9 @@ mod test { let index: u32 = 3; env.private_context(|context| { - mock_existing_handshake_secrets(Option::some(AppSiloedHandshakeSecrets { shared: secret, sender_only: 99 })); + mock_existing_handshake_secrets(Option::some( + AppSiloedHandshakeSecrets { shared: secret, sender_only: 99 }, + )); let _ = OracleMock::mock("aztec_prv_getNextTaggingIndex").returns(index); let resolve_oracle = OracleMock::mock("aztec_prv_resolveTaggingStrategy").returns( ResolvedTaggingStrategy::non_interactive_handshake(), @@ -130,6 +144,7 @@ mod test { OnchainDeliveryMode::onchain_unconstrained(), SENDER, RECIPIENT, + Option::none(), ), tag_from_secret_and_index(secret, index, OnchainDeliveryMode::onchain_unconstrained()), ); @@ -158,6 +173,7 @@ mod test { OnchainDeliveryMode::onchain_unconstrained(), SENDER, RECIPIENT, + Option::none(), ), tag_from_secret_and_index(secret, index, OnchainDeliveryMode::onchain_unconstrained()), ); @@ -172,7 +188,9 @@ mod test { let index: u32 = 3; env.private_context(|context| { - mock_existing_handshake_secrets(Option::some(AppSiloedHandshakeSecrets { shared: secret, sender_only: 99 })); + mock_existing_handshake_secrets(Option::some( + AppSiloedHandshakeSecrets { shared: secret, sender_only: 99 }, + )); let _ = OracleMock::mock("aztec_prv_getNextTaggingIndex").returns(index); let _ = OracleMock::mock("aztec_prv_isNullifierPending").returns(false); @@ -182,6 +200,7 @@ mod test { OnchainDeliveryMode::onchain_constrained(), SENDER, RECIPIENT, + Option::none(), ), tag_from_secret_and_index(secret, index, OnchainDeliveryMode::onchain_constrained()), ); @@ -191,6 +210,37 @@ mod test { }); } + #[test] + unconstrained fn derive_log_tag_honors_the_tag_derivation_override() { + let env = TestEnvironment::new(); + let secret: Field = 7; + let index: u32 = 3; + + env.private_context(|context| { + let _ = OracleMock::mock("aztec_prv_getAppTaggingSecret").returns(Option::some(secret)); + let _ = OracleMock::mock("aztec_prv_getNextTaggingIndex").returns(index); + // The wallet's default resolution would yield a different secret, which the override must bypass. + let resolve_oracle = OracleMock::mock("aztec_prv_resolveTaggingStrategy").returns( + ResolvedTaggingStrategy::unconstrained_secret(secret + 1), + ); + + // With a contract-fixed derivation, the tag comes from the override's secret, not the wallet's. + assert_eq( + derive_log_tag( + context, + OnchainDeliveryMode::onchain_unconstrained(), + SENDER, + RECIPIENT, + Option::some(TagDerivation::address_derived()), + ), + tag_from_secret_and_index(secret, index, OnchainDeliveryMode::onchain_unconstrained()), + ); + + // The override is honored: the wallet's default strategy resolution is never consulted. + assert_eq(resolve_oracle.times_called(), 0); + }); + } + unconstrained fn mock_existing_handshake_secrets(maybe_secrets: Option) { let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(maybe_secrets.serialize()); } 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 new file mode 100644 index 000000000000..c8b46f495c2a --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_derivation.nr @@ -0,0 +1,141 @@ +use crate::messages::delivery::{ + handshake::get_existing_app_siloed_handshake_secrets, tag_secret_source::TagSecretSource, +}; +use crate::oracle::notes::get_app_tagging_secret; +use crate::oracle::random::random; +use crate::protocol::address::AztecAddress; +use crate::standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS; + +global NON_INTERACTIVE_HANDSHAKE: u8 = 1; +global ADDRESS_DERIVED: u8 = 2; + +/// A contract's choice of tag-secret derivation. +/// +/// Selected through the on-chain delivery builder's `via_*` methods. On-chain delivery tags every message from a +/// secret shared between the sender and the recipient, and there is +/// [more than one way][`crate::messages::delivery::ResolvedTaggingStrategy`] to establish it. +/// Selecting a derivation instead lets the contract fix it, and its choice is honored over the wallet-resolved +/// strategy. +#[derive(Eq)] +pub(crate) struct TagDerivation { + kind: u8, +} + +impl TagDerivation { + /// Reuses an existing non-interactive handshake for the pair, creating a fresh one only when none exists. + pub(crate) fn non_interactive_handshake() -> Self { + Self { kind: NON_INTERACTIVE_HANDSHAKE } + } + + /// Derives the tag from the address-derived secret for the `(sender, recipient)` pair (a Diffie-Hellman exchange + /// between their addresses). + pub(crate) fn address_derived() -> Self { + Self { kind: ADDRESS_DERIVED } + } + + /// Asserts this choice's `kind` is a compile-time constant. + pub(crate) fn assert_kind_is_constant(self) { + assert_constant(self.kind as Field); + } + + /// Resolves this choice into the [`TagSecretSource`] backing the message tag. + pub(crate) fn into_tag_secret_source(self, 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(), + ) + } else { + // Safety: the secret is untrusted, but address-derived tagging is unconstrained-only, where a wrong + // secret only yields an undiscoverable tag. An invalid recipient has no shared secret, so we fall back + // to a random (undiscoverable) secret rather than failing the send. + let secret = unsafe { get_app_tagging_secret(sender, recipient).unwrap_or_else(|| random()) }; + TagSecretSource::unconstrained_secret(secret) + } + } +} + +/// Resolves to the secrets of a non-interactive handshake already registered for `(sender, recipient)`, or to +/// `fallback()` when none exists yet. +pub(crate) fn existing_handshake_secrets_or_else( + sender: AztecAddress, + recipient: AztecAddress, + fallback: fn[Env]() -> TagSecretSource, +) -> TagSecretSource { + // Safety: this only selects which source backs the tag. A reused handshake's secrets are constrained against + // the registry before a constrained tag is emitted; the fallback source constrains (or rejects) its own secret + // the same way. + let existing = + unsafe { get_existing_app_siloed_handshake_secrets(STANDARD_HANDSHAKE_REGISTRY_ADDRESS, sender, recipient) }; + existing.map_or_else(fallback, |secrets| TagSecretSource::existing_handshake(secrets)) +} + +mod test { + use crate::messages::delivery::handshake::AppSiloedHandshakeSecrets; + use crate::messages::delivery::tag_secret_source::TagSecretSource; + use crate::protocol::{address::AztecAddress, traits::{FromField, Serialize}}; + use crate::test::helpers::test_environment::TestEnvironment; + use super::TagDerivation; + use std::test::OracleMock; + + global SENDER: AztecAddress = AztecAddress::from_field(1); + global RECIPIENT: AztecAddress = AztecAddress::from_field(8); + + #[test] + unconstrained fn address_derived_uses_the_app_tagging_secret() { + let env = TestEnvironment::new(); + let secret: Field = 7; + + 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); + assert_eq(source, TagSecretSource::unconstrained_secret(secret)); + }); + } + + #[test] + unconstrained fn address_derived_falls_back_to_a_random_secret_for_an_invalid_recipient() { + let env = TestEnvironment::new(); + let random_secret: Field = 999; + + 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); + assert_eq(source, TagSecretSource::unconstrained_secret(random_secret)); + }); + } + + #[test] + unconstrained fn non_interactive_handshake_reuses_an_existing_handshake() { + let env = TestEnvironment::new(); + let secrets = AppSiloedHandshakeSecrets { shared: 7, sender_only: 99 }; + + env.private_context(|_context| { + mock_existing_handshake_secrets(Option::some(secrets)); + + let source = TagDerivation::non_interactive_handshake().into_tag_secret_source(SENDER, RECIPIENT); + assert_eq(source, TagSecretSource::existing_handshake(secrets)); + }); + } + + #[test] + unconstrained fn non_interactive_handshake_creates_a_fresh_one_when_none_exists() { + let env = TestEnvironment::new(); + + env.private_context(|_context| { + mock_existing_handshake_secrets(Option::none()); + + let source = TagDerivation::non_interactive_handshake().into_tag_secret_source(SENDER, RECIPIENT); + assert_eq(source, TagSecretSource::new_non_interactive_handshake()); + }); + } + + unconstrained fn mock_existing_handshake_secrets(maybe_secrets: Option) { + let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(maybe_secrets.serialize()); + } +} 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 13f9e408b24f..e880dda6f011 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 @@ -16,6 +16,15 @@ pub(crate) struct TagSecretSource { 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) + } +} + impl TagSecretSource { /// Reuses the secrets of a handshake already registered for the pair (mode-agnostic). pub(crate) fn existing_handshake(secrets: AppSiloedHandshakeSecrets) -> Self { diff --git a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr index ad14ffb89bc2..f7747e063047 100644 --- a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr +++ b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr @@ -2,17 +2,17 @@ use protocol_types::{address::AztecAddress, traits::FromField}; pub global STANDARD_AUTH_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x215c3cdec8b293135f02d3a9d217d18c446375ebae124dcde6e7854f7cc83c34, + 0x186fbbe15f50d01d67c471d5ed8b8be43b8826e77147cb0084302804e336e5d2, ); pub global STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x2cbc2589582f2912abd95bd63a7fb6f55eb3eefd961b7898dea6008b8f10525b, + 0x10a91e72a359e7441d4af9e418f14862abe994348970909d484ff2dad1594a58, ); pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x2aaa467f750cc6806cc19392045e8940d54763bf8c0cee9b8896b207ee01415a, + 0x1f6f565db58df81c2bbb8e9932fa92d2d1f537887a06e2cb965184f77a5235d2, ); pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x18f1fd280f678d062b43520d8e59b98b6b48a7c4ff7cd2dd7511799ffb0b4191, + 0x18233660a8797d74f63318c4223332e23b63f570da11fb142993ebc802a96728, ); diff --git a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr index bfe00801ff1d..2d718a4b271b 100644 --- a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr @@ -88,9 +88,13 @@ pub contract HandshakeRegistry { // The recipient is not involved in this note's delivery: they discover the handshake via the encrypted log // emitted below, not via the sender's note. We deliver onchain unconstrained rather than offchain so the - // note is discoverable via normal PXE sync. + // note is discoverable via normal PXE sync. This is a self-send (the note is owned by and delivered to + // `sender`), so the wallet would resolve to an address-derived secret anyway. Pinning that derivation here at + // compile time lets this contract, the handshake registry itself, skip the default flow's runtime registry + // check, which would otherwise embed the registry's own address in its bytecode and make its deploy address + // depend on itself. self.storage.handshakes.at(recipient).at(sender).initialize_or_replace(|_| note).deliver( - MessageDelivery::onchain_unconstrained().with_sender(sender), + MessageDelivery::onchain_unconstrained().with_sender(sender).via_address_derived_secret(), ); let log_tag = compute_log_tag( @@ -121,8 +125,10 @@ pub contract HandshakeRegistry { assert(note.app_siloed_secrets_for(self.msg_sender()) == secrets, "no matching handshake"); // `PrivateMutable::get_note` proves the current note by nullifying and recreating it. Deliver the - // replacement to the sender so later validation calls can prove the same current handshake again. - replacement_note_message.deliver(MessageDelivery::onchain_unconstrained()); + // replacement to the sender so later validation calls can prove the same current handshake again. As in + // `non_interactive_handshake`, the tag derivation is fixed to address-derived so the registry does not + // reference its own address when re-tagging its own note. + replacement_note_message.deliver(MessageDelivery::onchain_unconstrained().via_address_derived_secret()); } /// Returns the app-siloed secrets for an existing handshake. diff --git a/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz b/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz index b76b4f9f1178..4c440bec6a5c 100644 Binary files a/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz and b/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz differ diff --git a/yarn-project/standard-contracts/src/standard_contract_data.ts b/yarn-project/standard-contracts/src/standard_contract_data.ts index ff0cfb5c7c77..ca01c4fe5de8 100644 --- a/yarn-project/standard-contracts/src/standard_contract_data.ts +++ b/yarn-project/standard-contracts/src/standard_contract_data.ts @@ -20,21 +20,21 @@ export const StandardContractSalt: Record = { }; export const StandardContractAddress: Record = { - AuthRegistry: AztecAddress.fromStringUnsafe('0x215c3cdec8b293135f02d3a9d217d18c446375ebae124dcde6e7854f7cc83c34'), + AuthRegistry: AztecAddress.fromStringUnsafe('0x186fbbe15f50d01d67c471d5ed8b8be43b8826e77147cb0084302804e336e5d2'), MultiCallEntrypoint: AztecAddress.fromStringUnsafe( - '0x2cbc2589582f2912abd95bd63a7fb6f55eb3eefd961b7898dea6008b8f10525b', + '0x10a91e72a359e7441d4af9e418f14862abe994348970909d484ff2dad1594a58', ), - PublicChecks: AztecAddress.fromStringUnsafe('0x2aaa467f750cc6806cc19392045e8940d54763bf8c0cee9b8896b207ee01415a'), + PublicChecks: AztecAddress.fromStringUnsafe('0x1f6f565db58df81c2bbb8e9932fa92d2d1f537887a06e2cb965184f77a5235d2'), HandshakeRegistry: AztecAddress.fromStringUnsafe( - '0x18f1fd280f678d062b43520d8e59b98b6b48a7c4ff7cd2dd7511799ffb0b4191', + '0x18233660a8797d74f63318c4223332e23b63f570da11fb142993ebc802a96728', ), }; export const StandardContractClassId: Record = { - AuthRegistry: Fr.fromString('0x2f795da0b6834e302e01d3021b7ba65fc5e096e0720d4316459aff85734c13ec'), - MultiCallEntrypoint: Fr.fromString('0x2adc4a8074852763c4085173f9f9247bbabaf30ce462908587e49d6a95801128'), - PublicChecks: Fr.fromString('0x176c4d4be808251059342618e7489a203ab02f538d4b12498b9bd28c8f46bef5'), - HandshakeRegistry: Fr.fromString('0x02436e122c76e19caf303ccf5b04097414653b617f0c65aebb6f7d2fbd077b1d'), + AuthRegistry: Fr.fromString('0x1684ac4f8c250f8ecc1c7629a344fa36002f5130d3b1bbe6e2c0e73e14640f0a'), + MultiCallEntrypoint: Fr.fromString('0x003a615f16c5401ef184876baf2aa54f67ff693672a03134cc8920cfb58957c7'), + PublicChecks: Fr.fromString('0x086e79a9008498c03a83dd1e9b69b6cbe8d1fafe31b2a775d0b6610dac7d681c'), + HandshakeRegistry: Fr.fromString('0x1007fb490420810f54215128260e808c7ab2b1dc4c1801156833d4720696bf31'), }; export const StandardContractClassIdPreimage: Record< @@ -42,23 +42,23 @@ export const StandardContractClassIdPreimage: Record< { artifactHash: Fr; privateFunctionsRoot: Fr; publicBytecodeCommitment: Fr } > = { AuthRegistry: { - artifactHash: Fr.fromString('0x000b8f6131ef6d0dbd04b49b453f053551581e52f1db6d0b3f28c06bb225eda7'), + artifactHash: Fr.fromString('0x175607a9b31428948f4041db7d4e1667492292fb7850f8138c1d3cf5ffde8b34'), privateFunctionsRoot: Fr.fromString('0x17b584350f4c3ccafd8f688729afb9feab8976114fb40012e9dee65022c072a4'), publicBytecodeCommitment: Fr.fromString('0x2545f39893766508ce37bb5cea5e4dcab04c6f7f79f3089b1c076876e9d268b2'), }, MultiCallEntrypoint: { - artifactHash: Fr.fromString('0x0a608dfc9ec63f317c91fea449d3f9935d5e6cd2f2ec709bb4ed2ffc708c07a6'), + artifactHash: Fr.fromString('0x23af448187164c7d5e6f392346958e688690370515359de8909f3f05f2e26760'), privateFunctionsRoot: Fr.fromString('0x0e68dfbb256e80b08b3aef47aca1f2669e97a9c6259787893c1223ac083ad5d5'), publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, PublicChecks: { - artifactHash: Fr.fromString('0x26e9d24bd0619bc3663c056fe0e73db8c4d86d7695030898f65cbb48b4a05cf7'), + artifactHash: Fr.fromString('0x2f3869d9d89bf7b428da7f8c620aea11acc022cc0c7222544cc841e5ceedd8dc'), privateFunctionsRoot: Fr.fromString('0x202860adb1b8975971eeaf571aaaa88a27f4035290d58532ae7d60b0dfaad54c'), publicBytecodeCommitment: Fr.fromString('0x013c4f854a5c87c9daf86c5f9bc07a42c2a061f1d924a5b3564ec7edc8e18cb7'), }, HandshakeRegistry: { - artifactHash: Fr.fromString('0x1758549575a28f993d0be2f33e544c321bfb6e8dda5d969c2fef1025d5492c13'), - privateFunctionsRoot: Fr.fromString('0x13c6ea42ad92702a4690fa562e8dc3eb0e1f6e23abe883a5246c7140d7acd153'), + artifactHash: Fr.fromString('0x080bd5c37240f9425fea5c87b844a67ffda312dbf58c58288b7ba6b4d5db7663'), + privateFunctionsRoot: Fr.fromString('0x14fed008fa16ae6e45b7561aa0101d0d97953e2fd86caf7aaa603f14807797a8'), publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, }; @@ -96,13 +96,13 @@ export const StandardContractPrivateFunctions: Record< selector: FunctionSelector.fromField( Fr.fromString('0x00000000000000000000000000000000000000000000000000000000db548fcf'), ), - vkHash: Fr.fromString('0x17386be4ab92e7ac0c202d7027dc2424e6c35c06a9ac56e7bb01eb9dc9a2ef6c'), + vkHash: Fr.fromString('0x238417677997f9e919cd828f7bd156701e447f3d7c8881586f2277df607036d5'), }, { selector: FunctionSelector.fromField( Fr.fromString('0x00000000000000000000000000000000000000000000000000000000f1ff839b'), ), - vkHash: Fr.fromString('0x187d0ad185898fc8d5c06e2fe5e26889b124fa307602f152b3af5cfd988b0602'), + vkHash: Fr.fromString('0x12f43f384aa64661b012b52b1d80eb1d2202b8e459b50f56cfe270b0672c16ff'), }, ], };