Skip to content
Merged
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 @@ -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:

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

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

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

Expand Down
143 changes: 125 additions & 18 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr
Original file line number Diff line number Diff line change
@@ -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.
///
Expand All @@ -22,6 +23,7 @@ use super::mode::{DeliveryMode, OnchainDeliveryMode};
pub struct MessageDelivery {
mode: DeliveryMode,
sender_override: Option<AztecAddress>,
tag_derivation: Option<TagDerivation>,
}

impl MessageDelivery {
Expand All @@ -33,6 +35,10 @@ impl MessageDelivery {
self.sender_override
}

pub(crate) fn tag_derivation(self) -> Option<TagDerivation> {
self.tag_derivation
}

/// Delivers the message fully off-chain, with no guarantees whatsoever.
///
/// ## Use Cases
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
}
}

Expand All @@ -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<AztecAddress>,
tag_derivation: Option<TagDerivation>,
}

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.
Expand All @@ -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<OnchainUnconstrainedDelivery> 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<AztecAddress>,
tag_derivation: Option<TagDerivation>,
}

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<OnchainDelivery> for OnchainDeliveryMode {
fn from(delivery: OnchainDelivery) -> OnchainDeliveryMode {
delivery.mode
impl From<OnchainConstrainedDelivery> 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() {
Expand Down Expand Up @@ -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()));
}
}
18 changes: 16 additions & 2 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -80,6 +85,7 @@ where
recipient,
mode,
sender_override,
tag_derivation,
);
}
}
Expand All @@ -91,10 +97,18 @@ fn do_onchain_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
recipient: AztecAddress,
mode: DeliveryMode,
sender_override: Option<AztecAddress>,
tag_derivation: Option<TagDerivation>,
) {
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);

Expand All @@ -105,7 +119,7 @@ fn do_onchain_private_message_delivery<Env, let MESSAGE_PLAINTEXT_LEN: u32>(
|| 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
Expand Down
Loading
Loading