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 f4f94fbce7ee..5131e4534e66 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 @@ -167,17 +167,19 @@ Onchain delivery tags every message so the recipient can find it efficiently (se 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: +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 four strategies today: - **Non-interactive handshake**: the secret comes from a handshake published onchain that the recipient can derive. A non-recipient can at most learn that a sender did a handshake with the recipient, not the message itself, but the recipient discovers it without any prior coordination. Works for both constrained and unconstrained delivery. +- **Interactive handshake**: the secret comes from a handshake the recipient authorizes with a signature, which the registry requests through the custom request oracle and verifies in-circuit. Nothing announcing the handshake is published onchain (the recipient learns the secret while signing), but the recipient must be reachable to answer the request, or the send fails. Works for both constrained and unconstrained delivery. - **Address-derived secret**: the PXE derives the secret from the sender's and recipient's address keys via Diffie-Hellman. The wallet supplies no material, only the choice. It leaves no onchain trace, but the recipient only finds the message if they registered the sender in their PXE. Unconstrained delivery only. - **Arbitrary secret**: a raw secret point the two parties already share offchain, having coordinated out of band to agree on it. The wallet supplies the point and the PXE app-silos it. It leaves no onchain trace, but no onchain handshake backs the secret. Unconstrained delivery only. -| | Non-interactive handshake | Address-derived secret | Arbitrary secret | -|---|---|---|---| -| Onchain footprint when establishing | A handshake revealing information about the recipient | None | None | -| Who provides the material | The onchain registry | Nobody (PXE computes it) | The wallet (a raw point) | -| Constrained delivery | Supported | Not sound: not backed by an onchain handshake | Not sound: not backed by an onchain handshake | +| | Non-interactive handshake | Interactive handshake | Address-derived secret | Arbitrary secret | +|---|---|---|---|---| +| Onchain footprint when establishing | A handshake revealing information about the recipient | None | None | None | +| Who provides the material | The onchain registry | The onchain registry, with the recipient's signed authorization | Nobody (PXE computes it) | The wallet (a raw point) | +| Recipient coordination | None | The recipient must answer the signature request | The recipient must have registered the sender | Agreed out of band | +| Constrained delivery | Supported | Supported | Not sound: not backed by an onchain handshake | Not sound: not backed by an onchain handshake | ### Defaults @@ -186,6 +188,8 @@ When no `resolveTaggingSecretStrategy` hook is configured, the PXE applies a def - **Unconstrained delivery**: a non-interactive handshake when the recipient is external, so the recipient discovers the message without having registered the sender in advance. When the recipient is one of the wallet's own accounts (a self-send), an address-derived secret is used instead: the wallet holds both sides' keys, so no handshake is needed and nothing is revealed onchain. - **Constrained delivery**: a non-interactive handshake (constrained delivery must be backed by a handshake). +An interactive handshake is never the default, since it fails when the recipient cannot be reached: a wallet opts in through the hook when it knows how to reach them. + ### Configuring the strategy 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. @@ -198,7 +202,7 @@ A contract can fix the derivation at the point of delivery with the builder's `v 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. +Unconstrained delivery exposes `via_non_interactive_handshake()`, `via_interactive_handshake()` and `via_address_derived_secret()`. Constrained delivery exposes only `via_non_interactive_handshake()` and `via_interactive_handshake()`, since an address-derived secret cannot back constrained delivery. ## Note Discovery and the Sender 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 d726f9ee10b3..93244b41108d 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 @@ -27,7 +27,7 @@ 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 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. +There are four 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 four. ##### Arbitrary secret @@ -43,6 +43,12 @@ To establish a handshake, the sender publishes an ephemeral public key onchain, This lets the recipient discover messages from a sender they never registered, at the cost of publishing onchain that a handshake was made with them. It is the default for reaching a new external recipient, and unlike an address-derived or arbitrary secret it can back [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy). +##### Interactive handshake + +An interactive handshake derives the same kind of shared secret from an ephemeral key, but the ephemeral key is never published: instead of announcing it onchain, the recipient must be reachable at send time to sign it. The registry requests that signature through the custom request oracle and verifies it in-circuit, so the send fails if the recipient does not answer. The recipient learns the ephemeral key while signing and registers the resulting secret with their PXE so it can scan for the tags. As with a non-interactive handshake, the secret is app-siloed to the contract but left bare, with no directional fold. + +This reveals nothing onchain about the recipient, and like a non-interactive handshake it can back [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy). Because it requires the recipient's cooperation at send time, it is never the default: a wallet or contract opts in explicitly. + ##### Deriving the tag from the secret Whichever strategy produced it, the resulting app-siloed tagging secret is turned into a tag the same way: @@ -110,7 +116,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). 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)). +**c) Tagging with a handshake** - The sender and recipient establish a handshake to agree on a tagging secret, after which regular tagging works: the recipient can discover messages without having registered the sender in advance. A non-interactive handshake is published onchain, so it needs nothing from the recipient, but it reveals that someone did a handshake with them. An interactive handshake reveals nothing onchain; in exchange, the recipient must sign it at send time. 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/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md b/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md index dbdc302c3d4c..7d9fbfa86d59 100644 --- a/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md +++ b/docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md @@ -96,6 +96,8 @@ Pass a `resolveTaggingSecretStrategy` hook when [creating the PXE](#configuring- When the hook is absent, the PXE applies a default: both delivery modes use a [non-interactive handshake](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy) so the recipient can discover the message without prior coordination. +Returning `{ type: 'interactive-handshake' }` makes the handshake registry request the recipient's signed authorization through the [`resolveCustomRequest`](#resolvecustomrequest) hook (see the [example](#example-interactive-handshakes) below), so a wallet should only choose it when that request can be served. + ## `resolveCustomRequest` A general-purpose hook for *custom*, caller-defined requests. A contract reaches for it when it needs something it cannot get on its own: not from its local notes, not from the protocol's existing oracles. The contract gives the request a `kind` and an opaque `payload`, and the wallet returns an opaque response. Because the request is caller-defined, the wallet decides per `kind` how to answer, whether by reading state it holds, contacting another party, or fetching offchain data. 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 efd92c1019c6..cb8b3591b65b 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr @@ -134,9 +134,10 @@ impl MessageDelivery { /// /// The exception is the discovery tag. Reaching a recipient the sender has not handshaked with before establishes, /// by default, a non-interactive handshake. This reveals to anyone who knows the recipient's address that a - /// handshake was made with them, but not by whom, nor the contents. An interactive handshake would not leak this, - /// but it requires coordination with the recipient. Delivering to one of the wallet's own accounts instead uses an - /// address-derived secret and reveals nothing. + /// handshake was made with them, but not by whom, nor the contents. An interactive handshake + /// ([`OnchainUnconstrainedDelivery::via_interactive_handshake`]) does not leak this, but it requires the + /// recipient's cooperation. Delivering to one of the wallet's own accounts instead uses an address-derived + /// secret and reveals nothing. /// /// 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. @@ -187,7 +188,8 @@ impl MessageDelivery { /// The exception is the discovery tag. Constrained delivery is always backed by a handshake, established by /// default as a non-interactive one the first time the sender reaches the recipient. A non-interactive handshake /// reveals, to anyone who knows the recipient's address, that a handshake was made with them, but not by whom, - /// nor the contents. An interactive handshake would not leak this, but it requires coordination with the recipient. + /// nor the contents. An interactive handshake ([`OnchainConstrainedDelivery::via_interactive_handshake`]) does + /// not leak this, but it requires the recipient's cooperation. /// /// 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. @@ -257,6 +259,18 @@ impl OnchainUnconstrainedDelivery { *self } + /// Derives the discovery tag from an interactive handshake. + /// + /// Reuses an existing handshake for the pair, creating a fresh interactive one only when none exists. + /// + /// Unlike a non-interactive handshake, establishing an interactive one announces nothing onchain, but it requires + /// the recipient's cooperation: they must answer the registry's signed-authorization request, so the send fails + /// if they cannot be reached. + pub fn via_interactive_handshake(&mut self) -> Self { + self.tag_derivation = Option::some(TagDerivation::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. /// @@ -324,6 +338,19 @@ impl OnchainConstrainedDelivery { self.tag_derivation = Option::some(TagDerivation::non_interactive_handshake()); *self } + + /// Derives the discovery tag from an interactive handshake. + /// + /// Reuses an existing handshake for the pair, creating a fresh interactive one only when none exists. Overrides + /// the wallet's default resolution. + /// + /// Unlike a non-interactive handshake, establishing an interactive one announces nothing onchain, but it requires + /// the recipient's cooperation: they must answer the registry's signed-authorization request, so the send fails + /// if they cannot be reached. + pub fn via_interactive_handshake(&mut self) -> Self { + self.tag_derivation = Option::some(TagDerivation::interactive_handshake()); + *self + } } impl MessageDeliveryBuilder for OnchainConstrainedDelivery { @@ -402,5 +429,13 @@ mod test { 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())); + + let unconstrained_interactive = + MessageDelivery::onchain_unconstrained().via_interactive_handshake().build_message_delivery(); + assert_eq(unconstrained_interactive.tag_derivation(), Option::some(TagDerivation::interactive_handshake())); + + let constrained_interactive = + MessageDelivery::onchain_constrained().via_interactive_handshake().build_message_delivery(); + assert_eq(constrained_interactive.tag_derivation(), Option::some(TagDerivation::interactive_handshake())); } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr index e885e6c3318d..b5bd85eb6332 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr @@ -52,6 +52,8 @@ pub global GET_APP_SILOED_SECRETS_SELECTOR: FunctionSelector = comptime { FunctionSelector::from_signature("get_app_siloed_secrets((Field),(Field))") }; pub global NON_INTERACTIVE_HANDSHAKE_SELECTOR: FunctionSelector = comptime { FunctionSelector::from_signature("non_interactive_handshake((Field),(Field))") }; +pub global INTERACTIVE_HANDSHAKE_SELECTOR: FunctionSelector = + comptime { FunctionSelector::from_signature("interactive_handshake((Field),(Field))") }; pub global GET_HANDSHAKES_SELECTOR: FunctionSelector = comptime { FunctionSelector::from_signature("get_handshakes((Field),u32)") }; @@ -82,7 +84,7 @@ pub(crate) unconstrained fn get_existing_app_siloed_handshake_secrets( /// /// The registry inserts a fresh handshake note and returns the app-siloed secrets. The constrained return value is the /// source of truth for the secrets, so constrained delivery anchoring a freshly bootstrapped handshake needs no -/// separate `validate_handshake`. +/// separate call to the registry's `validate_handshake`. /// /// Any handshake already registered for the pair is overwritten. pub(crate) fn create_non_interactive_handshake( @@ -91,13 +93,33 @@ pub(crate) fn create_non_interactive_handshake( sender: AztecAddress, recipient: AztecAddress, ) -> AppSiloedHandshakeSecrets { - // TODO(F-660): dispatch to `perform_handshake(sender, recipient, handshake_type)` once interactive handshakes - // are supported. context .call_private_function(registry, NON_INTERACTIVE_HANDSHAKE_SELECTOR, [sender.to_field(), recipient.to_field()]) .get_preimage() } +/// Establishes an interactive handshake for `(sender, recipient)` and returns its app-siloed secrets. +/// +/// The registry obtains the recipient's signed authorization through a +/// [`resolve_custom_request`](crate::oracle::resolve_custom_request::resolve_custom_request) oracle call it resolves +/// internally, verifies it in-circuit, inserts a fresh handshake note and returns the secrets. Nothing announcing the +/// handshake is published onchain: the recipient learns the shared secret while signing. As with +/// [`create_non_interactive_handshake`], the constrained return value is the source of truth for the secrets, so +/// constrained delivery anchoring a freshly bootstrapped handshake needs no separate call to the registry's +/// `validate_handshake`. +/// +/// Any handshake already registered for the pair is overwritten. +pub(crate) fn create_interactive_handshake( + context: &mut PrivateContext, + registry: AztecAddress, + sender: AztecAddress, + recipient: AztecAddress, +) -> AppSiloedHandshakeSecrets { + context + .call_private_function(registry, INTERACTIVE_HANDSHAKE_SELECTOR, [sender.to_field(), recipient.to_field()]) + .get_preimage() +} + /// Fetches discovered handshakes from the HandshakeRegistry and derives app-siloed tagging secrets for each, /// returning them so that [`get_pending_tagged_logs`](crate::oracle::message_processing::get_pending_tagged_logs) /// searches for logs tagged with these secrets. 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..f387a482b847 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 @@ -3,6 +3,7 @@ use super::tag_secret_source::TagSecretSource; global NON_INTERACTIVE_HANDSHAKE: u8 = 1; global UNCONSTRAINED_SECRET: u8 = 2; +global INTERACTIVE_HANDSHAKE: u8 = 3; /// The tagging secret the PXE hands to the contract after applying the wallet's tagging secret strategy. It is already /// app-siloed, so the contract uses it directly to derive the message tag and never sees raw key material. @@ -23,6 +24,14 @@ impl ResolvedTaggingStrategy { Self { kind: UNCONSTRAINED_SECRET, secret } } + /// An interactive handshake: the recipient learns the secret while signing its authorization, and nothing + /// announcing the handshake is published onchain. See + /// [`create_interactive_handshake`](super::handshake::create_interactive_handshake) for how the registry obtains + /// that authorization. + pub fn interactive_handshake() -> Self { + Self { kind: INTERACTIVE_HANDSHAKE, secret: 0 } + } + pub fn is_unconstrained_secret(self) -> bool { self.kind == UNCONSTRAINED_SECRET } @@ -31,7 +40,8 @@ impl ResolvedTaggingStrategy { fn from_parts(kind: u8, secret: Field) -> Self { let resolved = Self { kind, secret }; assert( - ((kind == NON_INTERACTIVE_HANDSHAKE) & (secret == 0)) | (kind == UNCONSTRAINED_SECRET), + (((kind == NON_INTERACTIVE_HANDSHAKE) | (kind == INTERACTIVE_HANDSHAKE)) & (secret == 0)) + | (kind == UNCONSTRAINED_SECRET), f"unrecognized resolved tagging strategy kind: {kind}", ); resolved @@ -56,6 +66,8 @@ impl From for TagSecretSource { fn from(strategy: ResolvedTaggingStrategy) -> Self { if strategy.kind == NON_INTERACTIVE_HANDSHAKE { TagSecretSource::new_non_interactive_handshake() + } else if strategy.kind == INTERACTIVE_HANDSHAKE { + TagSecretSource::new_interactive_handshake() } else { TagSecretSource::unconstrained_secret(strategy.secret) } @@ -70,9 +82,11 @@ mod test { fn resolved_roundtrips_through_serialization() { let non_interactive = ResolvedTaggingStrategy::non_interactive_handshake(); let unconstrained_secret = ResolvedTaggingStrategy::unconstrained_secret(42); + let interactive = ResolvedTaggingStrategy::interactive_handshake(); assert(ResolvedTaggingStrategy::deserialize(non_interactive.serialize()) == non_interactive); assert(ResolvedTaggingStrategy::deserialize(unconstrained_secret.serialize()) == unconstrained_secret); + assert(ResolvedTaggingStrategy::deserialize(interactive.serialize()) == interactive); } #[test(should_fail_with = "unrecognized resolved tagging strategy kind")] @@ -84,4 +98,9 @@ mod test { fn deserializing_handshake_with_nonzero_secret_fails() { let _ = ResolvedTaggingStrategy::deserialize([1, 123]); } + + #[test(should_fail_with = "unrecognized resolved tagging strategy kind")] + fn deserializing_interactive_handshake_with_nonzero_secret_fails() { + let _ = ResolvedTaggingStrategy::deserialize([3, 123]); + } } 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 b0ae3cdc50cb..1f42d11d19c1 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 @@ -8,6 +8,7 @@ use crate::standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS; global NON_INTERACTIVE_HANDSHAKE: u8 = 1; global ADDRESS_DERIVED: u8 = 2; +global INTERACTIVE_HANDSHAKE: u8 = 3; /// A contract's choice of tag-secret derivation. /// @@ -22,11 +23,16 @@ pub(crate) struct TagDerivation { } impl TagDerivation { - /// Reuses an existing non-interactive handshake for the pair, creating a fresh one only when none exists. + /// Reuses an existing handshake for the pair, creating a fresh non-interactive one only when none exists. pub(crate) fn non_interactive_handshake() -> Self { Self { kind: NON_INTERACTIVE_HANDSHAKE } } + /// Reuses an existing handshake for the pair, creating a fresh interactive one only when none exists. + pub(crate) fn interactive_handshake() -> Self { + Self { kind: 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 { @@ -47,6 +53,13 @@ impl TagDerivation { recipient, || TagSecretSource::new_non_interactive_handshake(), ) + } else if self.kind == INTERACTIVE_HANDSHAKE { + // A fresh handshake is created in a constrained manner in `TagSecretSource::resolve_tag_secret`. + existing_handshake_secrets_or_else( + sender, + recipient, + || TagSecretSource::new_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 @@ -57,8 +70,8 @@ impl TagDerivation { } } -/// Resolves to the secrets of a non-interactive handshake already registered for `(sender, recipient)`, or to -/// `fallback()` when none exists yet. +/// Resolves to the secrets of a handshake already registered for `(sender, recipient)` (handshakes of either type +/// back both delivery modes), or to `fallback()` when none exists yet. pub(crate) fn existing_handshake_secrets_or_else( sender: AztecAddress, recipient: AztecAddress, @@ -135,6 +148,31 @@ mod test { }); } + #[test] + unconstrained fn 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::interactive_handshake().into_tag_secret_source(SENDER, RECIPIENT); + assert_eq(source, TagSecretSource::existing_handshake(secrets)); + }); + } + + #[test] + unconstrained fn 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::interactive_handshake().into_tag_secret_source(SENDER, RECIPIENT); + assert_eq(source, TagSecretSource::new_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 7f5adc86c0d5..3f7d65eaabba 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,7 +1,7 @@ use crate::context::PrivateContext; use crate::messages::delivery::{ constrained_delivery::{constrain_preexisting_handshake_secrets, emit_sequence_nullifier}, - handshake::{AppSiloedHandshakeSecrets, create_non_interactive_handshake}, + handshake::{AppSiloedHandshakeSecrets, create_interactive_handshake, create_non_interactive_handshake}, }; use crate::protocol::address::AztecAddress; use crate::standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS; @@ -9,6 +9,7 @@ use crate::standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS; global EXISTING_HANDSHAKE: u8 = 1; global NEW_NON_INTERACTIVE_HANDSHAKE: u8 = 2; global UNCONSTRAINED_SECRET: u8 = 3; +global NEW_INTERACTIVE_HANDSHAKE: u8 = 4; /// How a message's tag secret is sourced and, for constrained delivery, constrained. #[derive(Eq)] @@ -32,6 +33,13 @@ impl TagSecretSource { Self { kind: NEW_NON_INTERACTIVE_HANDSHAKE, tag_secret: 0, sender_only: 0 } } + /// Establishes a fresh interactive handshake, publishing nothing about the recipient onchain. See + /// [`create_interactive_handshake`](super::handshake::create_interactive_handshake) for how the registry obtains + /// the recipient's signed authorization. + pub(crate) fn new_interactive_handshake() -> Self { + Self { kind: NEW_INTERACTIVE_HANDSHAKE, tag_secret: 0, sender_only: 0 } + } + /// 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 { @@ -54,6 +62,15 @@ impl TagSecretSource { ); self.tag_secret = secrets.shared; self.sender_only = secrets.sender_only; + } else if self.kind == NEW_INTERACTIVE_HANDSHAKE { + let secrets = create_interactive_handshake( + context, + STANDARD_HANDSHAKE_REGISTRY_ADDRESS, + sender, + recipient, + ); + self.tag_secret = secrets.shared; + self.sender_only = secrets.sender_only; } self.tag_secret } @@ -78,7 +95,7 @@ impl TagSecretSource { secrets, index, ); - } else if self.kind == NEW_NON_INTERACTIVE_HANDSHAKE { + } else if (self.kind == NEW_NON_INTERACTIVE_HANDSHAKE) | (self.kind == NEW_INTERACTIVE_HANDSHAKE) { // The secrets were already obtained in a constrained manner, so we only need to verify that the index is 0. assert(index == 0, "freshly bootstrapped secret must start at index 0"); } else { @@ -153,6 +170,29 @@ mod test { }); } + #[test] + unconstrained fn new_interactive_handshake_resolves_and_constrains_the_bootstrapped_secrets() { + let env = TestEnvironment::new(); + let secrets = AppSiloedHandshakeSecrets { shared: 12, sender_only: 34 }; + + env.private_context(|context| { + // The registry's `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); + + let mut source = TagSecretSource::new_interactive_handshake(); + assert_eq(source.resolve_tag_secret(context, SENDER, RECIPIENT), secrets.shared); + + source.constrain_tag_secret(context, SENDER, RECIPIENT, 0); + assert_emitted_sequence_nullifier(context, secrets, 0); + }); + } + // A freshly bootstrapped handshake secret is the source of truth, so its sequence must start at index 0. #[test(should_fail_with = "freshly bootstrapped secret must start at index 0")] unconstrained fn fresh_handshake_secret_must_start_at_index_zero() { @@ -167,6 +207,19 @@ mod test { }); } + #[test(should_fail_with = "freshly bootstrapped secret must start at index 0")] + unconstrained fn fresh_interactive_handshake_secret_must_start_at_index_zero() { + let env = TestEnvironment::new(); + env.private_context(|context| { + TagSecretSource::new_interactive_handshake().constrain_tag_secret( + context, + AztecAddress::zero(), + AztecAddress::zero(), + 1, + ); + }); + } + #[test(should_fail_with = "an unconstrained tagging secret cannot back constrained delivery")] unconstrained fn unconstrained_secret_cannot_back_constrained_delivery() { let env = TestEnvironment::new(); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/version.nr b/noir-projects/aztec-nr/aztec/src/oracle/version.nr index 747d1289d2c6..1565ca3ebd37 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/version.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/version.nr @@ -11,7 +11,7 @@ /// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency /// without actually using any of the new oracles then there is no reason to throw. pub global ORACLE_VERSION_MAJOR: Field = 30; -pub global ORACLE_VERSION_MINOR: Field = 3; +pub global ORACLE_VERSION_MINOR: Field = 4; /// Asserts that the version of the oracle is compatible with the version expected by the contract. pub fn assert_compatible_oracle_version() { diff --git a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr index cb4192f78203..571a7a6e6d0a 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( - 0x0e7d3c56a185c40e4ce459eee03075b7dee2e9dc8f860157063afb3fde5ce097, + 0x1cac734679703a126df5fb4d9e40bbdae3b9a16777e46863f4a4f4271dce9390, ); pub global STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x03264f902d92f27dd0719cd6f5cc9c9d03ec6f5a48482e2158ebc2886e997210, + 0x09331cfb6aaa2ecf263cd325dd4a4eef9f5dbd2caf31e73bc6bc1cbc7c8c3eca, ); pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x05b4a7bf960bac46cc0c22aa6ee5b663a928787c9e7410fcf99e78182f634c0b, + 0x0bd3da430752caa1c67904e87e8be5cf076f4aacacd96d7efa30a05b86950b8f, ); pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x26f11691c7efea98db7eb7b4460cbc3f75db19772db2c2dbb8978979bcc5e388, + 0x2e2f664d40e3beafb08764dc30f365532dcb7b802efef74c0e36dfe6f48a989c, ); 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 a4f7b52d91ba..a1d7c8e8a9d3 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 @@ -3,6 +3,7 @@ use crate::protocol::{point::EmbeddedCurvePoint, traits::{Deserialize, Serialize global NON_INTERACTIVE_HANDSHAKE: u8 = 1; global ARBITRARY_SECRET: u8 = 2; global ADDRESS_DERIVED: u8 = 3; +global INTERACTIVE_HANDSHAKE: u8 = 4; /// How a message's tagging secret is chosen: the wallet's strategy. /// @@ -22,6 +23,14 @@ impl TaggingSecretStrategy { Self { kind: NON_INTERACTIVE_HANDSHAKE, secret: EmbeddedCurvePoint { x: 0, y: 0 } } } + /// A secret established through an interactive handshake. + /// + /// The registry obtains the recipient's signed authorization through a + /// [`resolve_custom_request`](crate::oracle::resolve_custom_request::resolve_custom_request) oracle call. + pub fn interactive_handshake() -> Self { + Self { kind: INTERACTIVE_HANDSHAKE, secret: EmbeddedCurvePoint { x: 0, y: 0 } } + } + /// A secret derived from the sender's and recipient's address keys via Diffie-Hellman. /// /// The PXE performs the key exchange, so the wallet supplies no material. @@ -40,7 +49,10 @@ impl TaggingSecretStrategy { fn from_parts(kind: u8, secret: EmbeddedCurvePoint) -> Self { let strategy = Self { kind, secret }; assert( - (((kind == NON_INTERACTIVE_HANDSHAKE) | (kind == ADDRESS_DERIVED)) & secret.is_infinite()) + ( + ((kind == NON_INTERACTIVE_HANDSHAKE) | (kind == ADDRESS_DERIVED) | (kind == INTERACTIVE_HANDSHAKE)) + & secret.is_infinite() + ) | (kind == ARBITRARY_SECRET), f"unrecognized tagging secret strategy kind: {kind}", ); @@ -74,10 +86,12 @@ mod test { let non_interactive = TaggingSecretStrategy::non_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(address.serialize()) == address); assert(TaggingSecretStrategy::deserialize(provided.serialize()) == provided); + assert(TaggingSecretStrategy::deserialize(interactive.serialize()) == interactive); } #[test(should_fail_with = "unrecognized tagging secret strategy kind")] @@ -94,4 +108,9 @@ mod test { fn deserializing_address_derived_with_noninfinite_point_fails() { let _ = TaggingSecretStrategy::deserialize([3, 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]); + } } 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 2d08d3e96664..b02994d2e6ca 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 @@ -86,6 +86,25 @@ 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( + 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); + + env.private_context_at(app, |_| { + let resolved = resolve_tagging_strategy( + AztecAddress::from_field(1), + recipient, + OnchainDeliveryMode::onchain_constrained(), + ); + assert_eq(resolved, ResolvedTaggingStrategy::interactive_handshake()); + }); +} + #[test] unconstrained fn applies_the_strategy_set_in_the_options() { let mut env = TestEnvironment::new_opts(TestEnvironmentOptions::new().with_tagging_secret_strategy( 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 c388db5e56cb..ad6a98ffcaae 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 @@ -22,7 +22,7 @@ use crate::protocol::{ /// /// 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 = 2; +pub global TXE_ORACLE_VERSION_MINOR: Field = 3; /// Asserts that the TXE oracle interface version is compatible. pub unconstrained fn assert_compatible_txe_oracle_version() { diff --git a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr index 3cc72d5cc988..6abcb812f212 100644 --- a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr @@ -5,7 +5,7 @@ use aztec::{ constrained_delivery::VALIDATE_HANDSHAKE_SELECTOR, handshake::{ AppSiloedHandshakeSecrets, GET_APP_SILOED_SECRETS_SELECTOR, GET_HANDSHAKES_SELECTOR, - MAX_HANDSHAKES_PER_PAGE, NON_INTERACTIVE_HANDSHAKE_SELECTOR, + INTERACTIVE_HANDSHAKE_SELECTOR, MAX_HANDSHAKES_PER_PAGE, NON_INTERACTIVE_HANDSHAKE_SELECTOR, }, }, oracle::shared_secret::get_shared_secret, @@ -56,6 +56,7 @@ unconstrained fn selectors_match_the_constrained_delivery_helper() { assert_eq(registry.get_app_siloed_secrets(sender, recipient).selector, GET_APP_SILOED_SECRETS_SELECTOR); assert_eq(registry.non_interactive_handshake(sender, recipient).selector, NON_INTERACTIVE_HANDSHAKE_SELECTOR); + assert_eq(registry.interactive_handshake(sender, recipient).selector, INTERACTIVE_HANDSHAKE_SELECTOR); assert_eq(registry.validate_handshake(sender, recipient, secrets).selector, VALIDATE_HANDSHAKE_SELECTOR); assert_eq(registry.get_handshakes(recipient, 0).selector, GET_HANDSHAKES_SELECTOR); } diff --git a/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/main.nr index 6274a64cb05c..0e8766f6de1d 100644 --- a/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/main.nr @@ -56,6 +56,14 @@ pub contract ConstrainedDeliveryTest { self.emit(DeliveryEvent { value }).deliver_to(recipient, MessageDelivery::onchain_constrained()); } + #[external("private")] + fn emit_event_via_interactive_handshake(recipient: AztecAddress, value: Field) { + self.emit(DeliveryEvent { value }).deliver_to( + recipient, + MessageDelivery::onchain_constrained().via_interactive_handshake(), + ); + } + #[external("private")] fn emit_two_events(recipient: AztecAddress) { self.emit(DeliveryEvent { value: 1 }).deliver_to(recipient, MessageDelivery::onchain_constrained()); diff --git a/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/test.nr b/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/test.nr index 45f4279d149e..d1fb70776c3b 100644 --- a/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/test/constrained_delivery_test_contract/src/test.nr @@ -66,6 +66,48 @@ unconstrained fn rehandshake_replaces_registry_secret_for_future_delivery() { assert_eq(next_index, 1); } +// The TXE environment configures no `resolveCustomRequest` resolver, so a fresh interactive handshake fails at the +// registry's signature request. Reaching that failure proves the builder option dispatches through the delivery stack +// to the registry's `interactive_handshake` entrypoint. +#[test(should_fail_with = "no resolveCustomRequest hook is configured")] +unconstrained fn interactive_handshake_dispatches_to_the_registry() { + let (env, registry_address, test_address, sender, recipient) = setup(); + let test_contract = ConstrainedDeliveryTest::at(test_address); + + let _ = env.call_private_opts( + sender, + authorizing(registry_address), + test_contract.emit_event_via_interactive_handshake(recipient, 1), + ); +} + +#[test] +unconstrained fn interactive_handshake_reuses_an_existing_registry_handshake() { + let (env, registry_address, test_address, sender, recipient) = setup(); + let test_contract = ConstrainedDeliveryTest::at(test_address); + let registry = HandshakeRegistry::at(registry_address); + + let _ = env.call_private(sender, registry.non_interactive_handshake(sender, recipient)); + let secret = env + .execute_utility_opts( + ExecuteUtilityOptions::new().with_from(test_address), + registry.get_app_siloed_secrets(sender, recipient), + ) + .expect(f"handshake should be siloed for the test contract") + .shared; + + // No resolver is configured, so this only succeeds because the existing handshake is reused instead of + // establishing a fresh interactive one. + env.call_private_opts( + sender, + authorizing(registry_address), + test_contract.emit_event_via_interactive_handshake(recipient, 1), + ); + + let next_index = env.call_private(sender, test_contract.next_index_for_secret(secret)); + assert_eq(next_index, 1); +} + #[test(should_fail_with = "reading an unknown nullifier")] unconstrained fn fails_at_index_above_zero_without_prior_nullifier() { let (env, registry_address, test_address, sender, recipient) = setup(); diff --git a/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz b/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz index 256ac97ad4c7..0765bf416e66 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/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 f82901466851..8b1930281d3e 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 @@ -3,6 +3,7 @@ import { Fr } from '@aztec/foundation/curves/bn254'; // Keep in sync with aztec::messages::delivery::ResolvedTaggingStrategy const NON_INTERACTIVE_HANDSHAKE = 1; const UNCONSTRAINED_SECRET = 2; +const INTERACTIVE_HANDSHAKE = 3; /** * The tagging secret PXE hands to the contract after applying the wallet's {@link TaggingSecretStrategy} choice @@ -13,6 +14,14 @@ export type ResolvedTaggingStrategy = /** A non-interactive handshake: the recipient re-derives the secret from the onchain handshake registry. */ type: 'non-interactive-handshake'; } + | { + /** + * An interactive handshake: the registry obtains the recipient's signed authorization through the custom + * request oracle, and the recipient learns the secret while signing. Nothing announcing the handshake is + * published onchain. + */ + type: 'interactive-handshake'; + } | { /** * An app-siloed, recipient-directional secret, ready to use. Only sound for unconstrained delivery. @@ -29,6 +38,8 @@ export function resolvedTaggingStrategyToFields(resolved: ResolvedTaggingStrateg return [new Fr(NON_INTERACTIVE_HANDSHAKE), Fr.ZERO]; case 'unconstrained-secret': return [new Fr(UNCONSTRAINED_SECRET), resolved.secret]; + case 'interactive-handshake': + return [new Fr(INTERACTIVE_HANDSHAKE), Fr.ZERO]; } } @@ -37,6 +48,8 @@ export function resolvedTaggingStrategyFromFields(kind: number, secret: Fr): Res switch (kind) { case NON_INTERACTIVE_HANDSHAKE: return { type: 'non-interactive-handshake' }; + case INTERACTIVE_HANDSHAKE: + return { type: 'interactive-handshake' }; case UNCONSTRAINED_SECRET: return { type: 'unconstrained-secret', secret }; default: diff --git a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts index 8e70f85d4175..a9bca1ccab5d 100644 --- a/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts +++ b/yarn-project/pxe/src/contract_function_simulator/oracle/private_execution_oracle.test.ts @@ -127,6 +127,40 @@ describe('PrivateExecutionOracle', () => { ); }); + it('resolves an interactive-handshake strategy', async () => { + const { oracle } = makeHookedOracle({ strategy: { type: 'interactive-handshake' } }); + + await expect(oracle.resolveTaggingStrategy(sender, recipient, AppTaggingSecretKind.CONSTRAINED)).resolves.toEqual( + { + type: 'interactive-handshake', + }, + ); + }); + + it('overrides a hooked interactive handshake on an unconstrained self-send with an address-derived secret', async () => { + const { oracle } = makeHookedOracle({ + strategy: { type: 'interactive-handshake' }, + keyStore: makeKeyStore({ ownsRecipient: true }), + }); + const secret = Fr.random(); + jest.spyOn(oracle, 'getAppTaggingSecret').mockResolvedValue(Option.some(secret)); + + await expect( + oracle.resolveTaggingStrategy(sender, recipient, AppTaggingSecretKind.UNCONSTRAINED), + ).resolves.toEqual({ type: 'unconstrained-secret', secret }); + }); + + it('keeps a hooked interactive handshake under constrained delivery even when the wallet owns the recipient', async () => { + const { oracle } = makeHookedOracle({ + strategy: { type: 'interactive-handshake' }, + keyStore: makeKeyStore({ ownsRecipient: true }), + }); + + await expect(oracle.resolveTaggingStrategy(sender, recipient, AppTaggingSecretKind.CONSTRAINED)).resolves.toEqual( + { type: 'interactive-handshake' }, + ); + }); + it('resolves an address-derived strategy to the unconstrained secret', async () => { const { oracle } = makeHookedOracle({ strategy: { type: 'address-derived' } }); const secret = Fr.random(); 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 cc08e3b25e2e..e8158f1f36cd 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 @@ -278,6 +278,8 @@ export class PrivateExecutionOracle extends UtilityExecutionOracle implements IP switch (strategy.type) { case 'non-interactive-handshake': return { type: 'non-interactive-handshake' }; + case 'interactive-handshake': + return { type: 'interactive-handshake' }; case 'address-derived': return this.#addressDerivedSecret(sender, recipient); case 'arbitrary-secret': { 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 aac3e9ce1fbd..6ca37cc05e68 100644 --- a/yarn-project/pxe/src/hooks/resolve_tagging_secret_strategy.ts +++ b/yarn-project/pxe/src/hooks/resolve_tagging_secret_strategy.ts @@ -3,6 +3,9 @@ import type { Point } from '@aztec/foundation/curves/grumpkin'; import type { AztecAddress } from '@aztec/stdlib/aztec-address'; import type { AppTaggingSecretKind } from '@aztec/stdlib/logs'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- only referenced by a {@link} doc tag +import type { ResolveCustomRequest } from './resolve_custom_request.js'; + /** * How a message's tagging secret is chosen: the wallet's strategy, returned by the `resolveTaggingSecretStrategy` hook * when no onchain handshake has been registered for the sender/recipient pair. This is intent (plus, for an arbitrary @@ -13,6 +16,14 @@ export type TaggingSecretStrategy = /** Establish a fresh non-interactive handshake via the onchain registry; reveals the recipient onchain. */ type: 'non-interactive-handshake'; } + | { + /** + * Establish a fresh interactive handshake via the onchain registry. Reveals nothing about the recipient + * onchain, but requires the recipient to answer the registry's signed-authorization request (served through + * the {@link ResolveCustomRequest} hook), so the send fails when it cannot be served. + */ + type: 'interactive-handshake'; + } | { /** Derive the secret from the sender's and recipient's address keys via ECDH. PXE computes and silos it. */ type: 'address-derived'; diff --git a/yarn-project/pxe/src/oracle_version.ts b/yarn-project/pxe/src/oracle_version.ts index 5fc369698f51..465664ab4a72 100644 --- a/yarn-project/pxe/src/oracle_version.ts +++ b/yarn-project/pxe/src/oracle_version.ts @@ -11,7 +11,7 @@ /// if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency without actually /// using any of the new oracles then there is no reason to throw. export const ORACLE_VERSION_MAJOR = 30; -export const ORACLE_VERSION_MINOR = 3; +export const ORACLE_VERSION_MINOR = 4; /// This hash is computed from the `ORACLE_REGISTRY` declaration (each oracle's name, ordered parameter names and /// types, and return type) and is used to detect when the oracle interface changes. When it does, you need to either: diff --git a/yarn-project/standard-contracts/src/standard_contract_data.ts b/yarn-project/standard-contracts/src/standard_contract_data.ts index 98960df510f9..10549c9ae8cb 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('0x0e7d3c56a185c40e4ce459eee03075b7dee2e9dc8f860157063afb3fde5ce097'), + AuthRegistry: AztecAddress.fromStringUnsafe('0x1cac734679703a126df5fb4d9e40bbdae3b9a16777e46863f4a4f4271dce9390'), MultiCallEntrypoint: AztecAddress.fromStringUnsafe( - '0x03264f902d92f27dd0719cd6f5cc9c9d03ec6f5a48482e2158ebc2886e997210', + '0x09331cfb6aaa2ecf263cd325dd4a4eef9f5dbd2caf31e73bc6bc1cbc7c8c3eca', ), - PublicChecks: AztecAddress.fromStringUnsafe('0x05b4a7bf960bac46cc0c22aa6ee5b663a928787c9e7410fcf99e78182f634c0b'), + PublicChecks: AztecAddress.fromStringUnsafe('0x0bd3da430752caa1c67904e87e8be5cf076f4aacacd96d7efa30a05b86950b8f'), HandshakeRegistry: AztecAddress.fromStringUnsafe( - '0x26f11691c7efea98db7eb7b4460cbc3f75db19772db2c2dbb8978979bcc5e388', + '0x2e2f664d40e3beafb08764dc30f365532dcb7b802efef74c0e36dfe6f48a989c', ), }; export const StandardContractClassId: Record = { - AuthRegistry: Fr.fromString('0x26418de27e7959352849f02436843dbdd033d69fa51a4bc8a60833c8b6fca502'), - MultiCallEntrypoint: Fr.fromString('0x19770ce1522e502ad5bcbc28e4d4affe96d1eeed61f366c87bc1e7214e44a567'), - PublicChecks: Fr.fromString('0x014a933a6759d143181575cf8bb9092f297ad9f6dceef20d5b0ba61369456a9b'), - HandshakeRegistry: Fr.fromString('0x187a5ae492f5eaf202d69290eb70b1f07315160434ba816a1ce22627bad4b6a1'), + AuthRegistry: Fr.fromString('0x2ae9560518defe569142b5ebfb1d91274d18630a314e4b3b724844bdf65191c7'), + MultiCallEntrypoint: Fr.fromString('0x18ac65ca493b64cdace6909bb1ad394f6cc90724cd629a61974384897488783c'), + PublicChecks: Fr.fromString('0x01eab218574ee39984a249d10f54be9270e5431720f6550434b701a1aadaa9c8'), + HandshakeRegistry: Fr.fromString('0x06c29e8d1f9c164dba7cba6b6d3d14501f9929d561bf51976e82e8eccf186299'), }; export const StandardContractClassIdPreimage: Record< @@ -42,22 +42,22 @@ export const StandardContractClassIdPreimage: Record< { artifactHash: Fr; privateFunctionsRoot: Fr; publicBytecodeCommitment: Fr } > = { AuthRegistry: { - artifactHash: Fr.fromString('0x1d2d505dc0a133d307dd6e121f9adda51718c5c61bf9239a277e3745897da935'), + artifactHash: Fr.fromString('0x073980bd8a697fd0161add1976094605e72db2c01994f7696242b86eab65eeed'), privateFunctionsRoot: Fr.fromString('0x17b584350f4c3ccafd8f688729afb9feab8976114fb40012e9dee65022c072a4'), publicBytecodeCommitment: Fr.fromString('0x2545f39893766508ce37bb5cea5e4dcab04c6f7f79f3089b1c076876e9d268b2'), }, MultiCallEntrypoint: { - artifactHash: Fr.fromString('0x1516cf4369b08c199f52a71fa2205bd13c51a53759f4a96571c48310bf026592'), + artifactHash: Fr.fromString('0x060a1486084d5064a08a0dd20c0d453b3983524c4b01134fe944bad598eae7bf'), privateFunctionsRoot: Fr.fromString('0x0e68dfbb256e80b08b3aef47aca1f2669e97a9c6259787893c1223ac083ad5d5'), publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, PublicChecks: { - artifactHash: Fr.fromString('0x04bdf54dbb0a90ccdd5659ad29b1b8206724e651c1d60f8046a0771496d26211'), + artifactHash: Fr.fromString('0x2af2b831c892ae978ed9445b0602696182a5286269c7a9bbd257be15c926297c'), privateFunctionsRoot: Fr.fromString('0x202860adb1b8975971eeaf571aaaa88a27f4035290d58532ae7d60b0dfaad54c'), publicBytecodeCommitment: Fr.fromString('0x013c4f854a5c87c9daf86c5f9bc07a42c2a061f1d924a5b3564ec7edc8e18cb7'), }, HandshakeRegistry: { - artifactHash: Fr.fromString('0x1c4cdc2834480eed826aeece9b85ffb7bdfe751a01bc76220e3fa12527a9c932'), + artifactHash: Fr.fromString('0x25bb3f7badae6b5210b95d3aba3806164fbff87213cede294ea2069902da0eea'), privateFunctionsRoot: Fr.fromString('0x02a4dba36389845b8ef0108562f7536d3284f07ca678558fc3c3bce3b24ee821'), publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, diff --git a/yarn-project/txe/src/oracle/txe_oracle_registry.ts b/yarn-project/txe/src/oracle/txe_oracle_registry.ts index 3e1d90509638..de2cf484721f 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_registry.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_registry.ts @@ -70,6 +70,7 @@ const GAS_SETTINGS: TypeMapping = { const STRATEGY_NON_INTERACTIVE_HANDSHAKE = 1; const STRATEGY_ARBITRARY_SECRET = 2; const STRATEGY_ADDRESS_DERIVED = 3; +const STRATEGY_INTERACTIVE_HANDSHAKE = 4; const TAGGING_SECRET_STRATEGY: TypeMapping = { serialization: { @@ -77,6 +78,8 @@ const TAGGING_SECRET_STRATEGY: TypeMapping = { switch (strategy.type) { case 'non-interactive-handshake': return [new Fr(STRATEGY_NON_INTERACTIVE_HANDSHAKE), Fr.ZERO, Fr.ZERO]; + case 'interactive-handshake': + return [new Fr(STRATEGY_INTERACTIVE_HANDSHAKE), Fr.ZERO, Fr.ZERO]; case 'address-derived': return [new Fr(STRATEGY_ADDRESS_DERIVED), Fr.ZERO, Fr.ZERO]; case 'arbitrary-secret': @@ -91,6 +94,8 @@ const TAGGING_SECRET_STRATEGY: TypeMapping = { switch (kind) { case STRATEGY_NON_INTERACTIVE_HANDSHAKE: return { type: 'non-interactive-handshake' }; + case STRATEGY_INTERACTIVE_HANDSHAKE: + return { type: 'interactive-handshake' }; case STRATEGY_ADDRESS_DERIVED: return { type: 'address-derived' }; case STRATEGY_ARBITRARY_SECRET: diff --git a/yarn-project/txe/src/oracle/txe_oracle_version.ts b/yarn-project/txe/src/oracle/txe_oracle_version.ts index 19351a2cbc7b..4c132ebd2a81 100644 --- a/yarn-project/txe/src/oracle/txe_oracle_version.ts +++ b/yarn-project/txe/src/oracle/txe_oracle_version.ts @@ -6,7 +6,7 @@ * 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 = 2; +export const TXE_ORACLE_VERSION_MINOR = 3; /** * This hash is computed from the TXE oracle interfaces (IAvmExecutionOracle and ITxeExecutionOracle) and is used to