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 @@ -111,7 +111,7 @@ const publicLogs = block?.body.txEffects.flatMap(tx => tx.publicLogs) ?? [];

Event data published onchain is stored in Ethereum blobs, which incurs costs. Consider:

- Use `OFFCHAIN` delivery for lower costs when you have custom delivery infrastructure
- Use offchain delivery for lower costs when you have custom delivery infrastructure
Comment thread
vezenovm marked this conversation as resolved.
- Only emit events when necessary for your application's functionality

## Next steps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ See the [aztec.js documentation](../../aztec-js/index.md) for more details on ac

**Onchain delivery with no content guarantees.**

This mode provides the same low proving time as `OFFCHAIN` while avoiding the need to implement custom delivery infrastructure. The tradeoff: you pay for DA (blob space) without gaining additional guarantees. If you're willing to build offchain delivery, use `OFFCHAIN` instead - it's strictly cheaper with the same guarantees.
This mode provides the same low proving time as offchain delivery while avoiding the need to implement custom delivery infrastructure. The tradeoff: you pay for DA (blob space) without gaining additional guarantees. If you're willing to build offchain delivery, use it instead - it's strictly cheaper with the same guarantees.

- **Use when:** The sender is incentivized to deliver correctly but you don't want to implement offchain delivery infrastructure
- **Costs:** DA gas fees for the encrypted log, zero proving time overhead
- **Guarantees:** Message stored onchain and retrievable, but sender can deliver incorrect content or wrong tag
- **Privacy:** High - encrypted log reveals minimal information
- **Privacy:** High for the message contents. By default, reaching a recipient the sender has not handshaked with before establishes a handshake that reveals the recipient was contacted (see [Tagging secret strategy](#tagging-secret-strategy))

```rust
// Minting to an admin who controls the contract
Expand All @@ -140,12 +140,10 @@ self.storage.balances.at(admin).add(amount)

**Onchain delivery with guaranteed correct content.**

**WARNING**: This mode is [currently NOT fully constrained](https://github.com/AztecProtocol/aztec-packages/issues/14565). The log's tag is unconstrained, meaning a malicious sender could prevent the recipient from finding the message.

- **Use when:** The sender cannot be trusted to deliver correctly (e.g., paying fees, creating notes for others, multisig configuration changes). Use this when you need to prove to a contract that the delivery has been done correctly. You can imagine a private NFT sale escrow contract where the escrow would be holding the NFT (the contract itself would be the NFT note owner) and then the escrow would release the NFT to the buyer once the NFT buyer pays the seller. In this case the `NFTSale::buy(...)` function would trigger the payment token transfer from the buyer to the seller and it would need to use `ONCHAIN_CONSTRAINED` delivery otherwise the escrow contract would be willing to transfer the NFT without the NFT seller actually being able to then spend the money. Note that for the transfer of the NFT from the escrow contract to the buyer you could use `OFFCHAIN` delivery because the delivery and encryption would be done in the buyer's PXE and hence there is alignment.
- **Use when:** The sender cannot be trusted to deliver correctly (e.g., paying fees, creating notes for others, multisig configuration changes). Use this when you need to prove to a contract that the delivery has been done correctly. You can imagine a private NFT sale escrow contract where the escrow would be holding the NFT (the contract itself would be the NFT note owner) and then the escrow would release the NFT to the buyer once the NFT buyer pays the seller. In this case the `NFTSale::buy(...)` function would trigger the payment token transfer from the buyer to the seller and it would need to use constrained delivery otherwise the escrow contract would be willing to transfer the NFT without the NFT seller actually being able to then spend the money. Note that for the transfer of the NFT from the escrow contract to the buyer you could use offchain delivery because the delivery and encryption would be done in the buyer's PXE and hence there is alignment.
- **Costs:** DA gas fees for the encrypted log, proving time overhead for encryption and tagging
- **Guarantees:** Recipient receives correctly encrypted content (once tag constraining is implemented, recipient will be able to find it)
- **Privacy:** High - encrypted log reveals minimal information
- **Guarantees:** Recipient will always be able to find correctly encrypted content: both the encryption and the discovery tag are constrained and stored onchain.
- **Privacy:** High for the message contents. By default, reaching a recipient the sender has not handshaked with before establishes a handshake that reveals the recipient was contacted (see [Tagging secret strategy](#tagging-secret-strategy))

```rust
// Minting to an arbitrary recipient - must guarantee delivery
Expand All @@ -157,9 +155,9 @@ self.storage.balances.at(recipient).add(amount)

Ask yourself: **"Is the sender incentivized to deliver this note correctly?"**

- **Yes, and they can contact the recipient offchain** Use `OFFCHAIN`
- **Yes, but they cannot or prefer not to contact them offchain or you don't want to implement offchain delivery** Use `ONCHAIN_UNCONSTRAINED`
- **No, the sender might not deliver correctly** Use `ONCHAIN_CONSTRAINED`
- **Yes, and they can contact the recipient offchain** Use offchain delivery
- **Yes, but they cannot or prefer not to contact them offchain or you don't want to implement offchain delivery** Use unconstrained delivery
- **No, the sender might not deliver correctly** Use constrained delivery

## Tagging secret strategy

Expand All @@ -179,10 +177,10 @@ The wallet's answer is a **tagging secret strategy**: it expresses *which* secre

### Defaults

When no `resolveTaggingSecretStrategy` hook is configured, the PXE applies a privacy-safe default:
When no `resolveTaggingSecretStrategy` hook is configured, the PXE applies a default:

- **Unconstrained delivery**: an address-derived (Diffie-Hellman) shared secret. It leaves no onchain trace, but the recipient only finds the message if they registered the sender in their PXE.
- **Constrained delivery**: fails, rather than silently revealing the recipient through a non-interactive handshake.
- **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).

### Configuring the strategy

Expand Down
2 changes: 1 addition & 1 deletion docs/docs-developers/docs/aztec-nr/standards/escrow.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ pub fn _share_escrow(
}
```

`_get_escrow` reconstructs the escrow address deterministically from the calling contract's address (used as the salt) and a set of master secret keys. `_share_escrow` emits an encrypted log so that the designated `account` can discover the escrow address and the keys needed to access its notes. Without this notification, the user's PXE would have no way to find the escrow or decrypt notes held there. The `ONCHAIN_CONSTRAINED` delivery mode ensures the log is validated against the note hash tree before the recipient's PXE trusts it.
`_get_escrow` reconstructs the escrow address deterministically from the calling contract's address (used as the salt) and a set of master secret keys. `_share_escrow` emits an encrypted log so that the designated `account` can discover the escrow address and the keys needed to access its notes. Without this notification, the user's PXE would have no way to find the escrow or decrypt notes held there. Constrained delivery ensures the log is validated against the note hash tree before the recipient's PXE trusts it.
Original file line number Diff line number Diff line change
Expand Up @@ -27,43 +27,43 @@ 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.

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.

##### Arbitrary secret

Two parties that already share a secret point out of band can use it directly. PXE app-siloes the point to the contract (`poseidon2(point.x, point.y, contract)`) and then folds in the recipient (`poseidon2(appSecret, recipient)`) to make the secret directional, so tags from Alice to Bob differ from tags from Bob to Alice. The recipient registers the secret point with their PXE so it can scan for the resulting tags. This leaves no onchain trace, but nothing onchain backs the secret, so it cannot be used for [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy).

##### Address-derived secret

When the secret is derived from addresses, the sender and recipient compute a value specific to their pair and the contract through a layered hashing process:
An address-derived secret is the same, except the shared secret point is computed instead of supplied. The sender and recipient each derive it via Diffie-Hellman on the Grumpkin curve from their own [incoming viewing secret key](../../accounts/keys.md#incoming-viewing-keys) (`ivsk`) and the other party's address point: `S = (preaddress + ivsk) × AddressPoint`. The app-siloing and directional fold are then identical. Because the point comes from the parties' addresses, the recipient registers the sender's address with their PXE (rather than a secret point) so it can compute the tags. Like an arbitrary secret, it cannot back [constrained delivery](../../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy).

##### Non-interactive handshake

To establish a handshake, the sender publishes an ephemeral public key onchain, encrypted to the recipient, under a log tagged with the recipient's address. During sync the recipient scans for handshakes addressed to them, decrypts the ephemeral key, and derives the shared secret via Diffie-Hellman against their own `ivsk`. Because that secret is already derived against the recipient, it is app-siloed to the contract but left bare, with no directional fold.

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).

##### Deriving the tag from the secret

Whichever strategy produced it, the resulting app-siloed tagging secret is turned into a tag the same way:

```mermaid
flowchart LR
A[Sender ivsk + Address] --> B["Shared Secret (DH)"]
C[Recipient Address Point] --> B
B --> D{{"poseidon2(S.x, S.y, contract)"}}
D --> E[App Tagging Secret]
E --> F{{"poseidon2(appSecret, recipient)"}}
F --> G[Directional Secret]
G --> H{{"poseidon2(secret, index)"}}
G["Tagging Secret"] --> H{{"poseidon2(secret, index)"}}
H --> I[Tag]
I --> J{{"silo(contract, tag)"}}
J --> K[Siloed Tag]
```

The derivation has four stages:

1. **Shared secret**: The sender and recipient compute the same shared secret via Diffie-Hellman key exchange on the Grumpkin curve. Each party uses their [incoming viewing secret key](../../accounts/keys.md#incoming-viewing-keys) (`ivsk`) and the other party's address point: `S = (preaddress + ivsk) × AddressPoint`.

2. **App tagging secret**: The shared secret is hashed with the contract address to produce a per-contract secret: `poseidon2(S.x, S.y, contract_address)`. This ensures tags from different contracts cannot be linked.

3. **Directional secret**: The app secret is hashed with the recipient address: `poseidon2(appSecret, recipient)`. This makes the secret asymmetric — tags from Alice to Bob differ from tags from Bob to Alice.

4. **Tag**: The directional secret is hashed with an index (a counter that increments for each log the sender emits to this recipient in this contract): `poseidon2(directionalSecret, index)`.

When the log is emitted, the protocol kernel **siloes** the tag with the contract address before it appears onchain. This siloed tag is what the node stores and indexes. Both the sender and recipient can independently compute the siloed tags and use them to query the node.
The tagging secret is hashed with an index, a counter that increments for each log the sender emits to this recipient in this contract: `poseidon2(secret, index)`. When the log is emitted, the protocol kernel **siloes** the resulting tag with the contract address before it appears onchain. This siloed tag is what the node stores and indexes. Both the sender and recipient can independently compute the siloed tags and use them to query the node.

#### The sender in note tagging

The "sender" in note tagging is **not necessarily the transaction sender**. It's the **sender for tags**, which the wallet supplies as a default (typically the originating account address). Contracts can override this at message delivery by using `with_sender`, for both constrained and unconstrained delivery, e.g. `MessageDelivery::onchain_constrained().with_sender(address)`.

#### Registering known senders

To discover notes from a particular sender, the recipient's PXE must know the sender's address in advance so it can compute the shared tagging secret. Register senders using the wallet API:
To discover notes from a particular sender via an [address-derived secret](#address-derived-secret), the recipient's PXE must know the sender's address in advance so it can compute the shared tagging secret. Register senders using the wallet API:

```typescript
// Register a sender so your PXE can discover notes from them
Expand All @@ -76,7 +76,7 @@ Notes sent to yourself are always discoverable — the PXE automatically adds al

The `#[aztec]` macro automatically injects an unconstrained `sync_state` utility function into every contract. This function is invoked by the PXE during note syncing to orchestrate discovery via oracles; manual execution is forbidden by the PXE to prevent inconsistencies. The process works as follows:

1. **Fetch tagged logs**: The contract calls the `fetchTaggedLogs` oracle. The PXE computes tags for every (sender, recipient) pair it knows about, queries the node for matching logs, and returns them to the contract.
1. **Fetch tagged logs**: The contract calls the `fetchTaggedLogs` oracle. The PXE computes tags for every secret it can use for this recipient, queries the node for matching logs, and returns them to the contract.

2. **Decrypt**: For each log, the contract strips the tag and attempts AES-128 decryption using a symmetric key derived from the recipient's private key (via ECDH). Logs that don't decrypt are silently discarded (they were not intended for this recipient).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ When the hook is absent, cross-contract utility calls are denied. See [Cross-con

Called as a fallback for message delivery: a registered onchain handshake's secret is reused directly, so this hook only fires when the sender-recipient pair has none yet. The wallet returns a concrete `TaggingSecretStrategy` (and any material the chosen derivation needs); see [Tagging secret strategy](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy) for the variants, the trade-offs, and the defaults in each environment.

For an unconstrained self-send (the recipient is one of the wallet's own accounts), the PXE always uses an [address-derived shared secret](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy) regardless of what the hook returns: both sides' keys are local, so no handshake is needed.

### In Noir tests

When testing in Noir, leaving the strategy unset makes `TestEnvironment` fall back to the bare PXE default. Set a strategy when creating the environment to exercise a specific one; it affects message delivery in private executions:
Expand All @@ -92,7 +94,7 @@ let env = TestEnvironment::new_opts(

Pass a `resolveTaggingSecretStrategy` hook when [creating the PXE](#configuring-hooks). It receives a `TaggingSecretStrategyRequest` with the executing contract's address and the message's sender, recipient, and delivery mode (`'constrained'` or `'unconstrained'`), so a wallet can apply per-application or per-recipient policies, or surface the decision to the user, instead of returning a fixed value.

When the hook is absent, the PXE applies a privacy-safe default: unconstrained delivery uses an [address-derived shared secret](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy), which leaves no onchain trace, while constrained delivery fails rather than silently revealing the recipient through a [non-interactive handshake](../../aztec-nr/framework-description/note_delivery.md#tagging-secret-strategy).
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.

## `resolveCustomRequest`

Expand Down
6 changes: 6 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [PXE] Unconstrained delivery defaults to a non-interactive handshake for external recipients

When no `resolveTaggingSecretStrategy` hook is configured, onchain unconstrained delivery now defaults to a non-interactive handshake when the recipient is external (an account whose keys the wallet does not hold), instead of an address-derived shared secret. A self-send (the recipient is one of the wallet's own accounts) still uses an address-derived secret, which needs no handshake and leaves no onchain trace.

**Impact**: An external recipient can now discover unconstrained-delivered messages without having registered the sender in advance, but establishing the handshake publishes an onchain marker derived from the recipient's address (anyone who knows that address can tell a handshake was created for them, though not by whom nor the contents). Wallets that want the previous behavior can configure a `resolveTaggingSecretStrategy` hook that returns an `address-derived` strategy.

### [Aztec.nr] `PrivateContext` data fields are no longer public

`PrivateContext`'s data fields are now private (or crate-internal): its public API is now exclusively its methods. Contracts that read these fields directly must switch to the corresponding getter. A new `get_side_effect_counter()` getter exposes the side-effect counter, and a new `is_static_call()` getter replaces reaching into `inputs.call_context`. The `get_anchor_block_header()` getter already existed.
Expand Down
Loading
Loading