Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
43 changes: 39 additions & 4 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()));
}
}
28 changes: 25 additions & 3 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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)") };

Expand Down Expand Up @@ -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(
Expand All @@ -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.
Expand Down
Loading
Loading