Skip to content

Commit 7d8a4e3

Browse files
author
AztecBot
committed
Merge branch 'v5-next' into merge-train/fairies-v5
2 parents 0bb0628 + 79a65f9 commit 7d8a4e3

4 files changed

Lines changed: 165 additions & 52 deletions

File tree

docs/docs-developers/docs/aztec-nr/framework-description/note_delivery.md

Lines changed: 43 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ In Aztec, creating a note involves two steps:
1515

1616
Without delivery, the recipient won't know the note exists or be able to access its contents, even though the note hash is onchain.
1717

18-
## The `.deliver()` Method
18+
## The `.deliver()` method
1919

2020
When you create a note using state variables like `PrivateMutable`, `PrivateSet`, `BalanceSet`, or `SinglePrivateMutable`, the creation methods return a `NoteMessage` or `MaybeNoteMessage` object. A message contains arbitrary information emitted from a contract - currently this includes notes and private events, though developers may define other message types in the future. You must call `.deliver()` on this object to send the message (containing the note) to the recipient.
2121

@@ -43,21 +43,23 @@ Aztec provides three delivery modes that offer different tradeoffs between cost,
4343

4444
This delivery method encrypts messages without constraints and emits them via an oracle call as offchain effects, rather than through the protocol's log stream (which would post data to Ethereum blobs). With offchain delivery, you must manually handle both message transmission and processing.
4545

46-
#### How It Works
46+
#### How it works
4747

4848
Offchain messages bypass Aztec's default private log infrastructure entirely:
4949

5050
1. **Message emission**: The contract encrypts the message (without constraints) and emits it via an oracle call. This creates an "offchain effect" that is included in the transaction but not posted to L1.
5151

52-
2. **Manual extraction**: When the transaction is sent, you must extract the offchain message from the transaction's offchain effects (available via `provenTx.offchainEffects` in aztec.js).
52+
2. **Extraction**: When the transaction is sent, you read the emitted messages from the send result (`offchainMessages` in aztec.js), or extract them from a proven transaction's offchain effects.
5353

54-
3. **Manual delivery**: You deliver the message through your own channel - Signal, cloud storage, QR codes, peer-to-peer networks, etc.
54+
3. **Manual delivery**: You deliver the message through your own channel - a claim link, QR code, messaging service, cloud storage, etc.
5555

56-
4. **Manual processing**: The recipient calls `process_message` on the target contract (as an unconstrained function), passing the ciphertext and message context. This decrypts the message and processes it (e.g., adding notes to the PXE database).
56+
4. **Receipt and processing**: The recipient calls the `offchain_receive` utility function on the contract that emitted the message (this function is generated automatically by the `#[aztec]` macro). The message is stored in a local inbox and processed during private state sync once the transaction that emitted it is found onchain.
5757

58-
The PXE cannot automatically discover offchain messages during private state sync because they are not in the log stream that nodes load from Ethereum blobs. **You are responsible for implementing both the delivery mechanism and ensuring the recipient processes the message.**
58+
The PXE cannot automatically discover offchain messages during private state sync because they are not in the log stream that nodes load from Ethereum blobs. **You are responsible for implementing the delivery mechanism and ensuring the recipient receives the message.**
5959

60-
#### When to Use
60+
See [Offchain message delivery](./offchain_message_delivery.md) for the complete workflow, including sender and recipient code, message processing details, and how to test it from Noir.
61+
62+
#### When to use
6163

6264
- **Use when:** The sender is incentivized to deliver correctly (e.g., sending to yourself, payment for goods/services where recipient must receive the note to complete the transaction)
6365
- **Costs:** Zero delivery fees (no blob space), zero proving time overhead
@@ -66,7 +68,7 @@ The PXE cannot automatically discover offchain messages during private state syn
6668

6769
This is expected to be the most common delivery method when you don't need constrained delivery guarantees, as it completely eliminates blob space costs.
6870

69-
#### Example Use Cases
71+
#### Example use cases
7072

7173
- Change notes when transferring tokens (you're sending to yourself)
7274
- Payments where the recipient won't provide goods/services without the note
@@ -79,45 +81,36 @@ self.storage.balances.at(sender).add(change_amount)
7981
.deliver(MessageDelivery::offchain());
8082
```
8183

82-
:::info TODO
83-
This section will be updated with a complete TypeScript example showing how to extract offchain messages from transaction effects and manually deliver them once the API in Aztec.js is finalized. The full workflow example will make the offchain delivery pattern clearer.
84-
:::
85-
86-
#### JavaScript Implementation
84+
#### JavaScript implementation
8785

88-
When using offchain delivery, extract and manually deliver messages in your application:
86+
When using offchain delivery, extract the emitted messages from the send result and deliver them to the recipient in your application. The recipient hands each message to their wallet via the auto-generated `offchain_receive` utility function:
8987

9088
```typescript
91-
import { MessageContext } from "@aztec/stdlib/logs"
92-
93-
// Prove transaction and get offchain effects
94-
const txProvingResult = await wallet.pxe.proveTx(txRequest);
95-
const provenTx = new ProvenTx(
96-
wallet.node,
97-
await txProvingResult.toTx(),
98-
txProvingResult.getOffchainEffects(),
99-
txProvingResult.stats,
100-
);
101-
102-
// Extract offchain message
103-
const offchainEffects = provenTx.offchainEffects;
104-
const ciphertext = offchainEffects[0].data.slice(2);
105-
106-
// Send tx
107-
const sentTx = provenTx.send()
108-
const tx = await sentTx.wait()
109-
const txHash = await sentTx.getTxHash()
110-
111-
// Deliver via your chosen channel (e.g., send to recipient via Signal, cloud storage, etc.). This is what you'd have to implement
112-
await deliverViaMyChannel(ciphertext, recipient);
113-
114-
// Recipient processes the message
115-
const txEffect = await aztecNode.getTxEffect(txHash);
116-
const messageContext = MessageContext.fromTxEffectAndRecipient(txEffect, recipient);
117-
await contract.methods.process_message(ciphertext, messageContext.toNoirStruct()).simulate();
89+
// Sender: send the transaction and extract the emitted offchain messages
90+
const { receipt, offchainMessages } = await token.methods
91+
.transfer_in_private_with_offchain_delivery(alice, bob, amount, 0)
92+
.send({ from: alice });
93+
94+
const messageForBob = offchainMessages.find((msg) => msg.recipient.equals(bob));
95+
96+
// Deliver via your chosen channel (e.g. a claim link, QR code, messaging service).
97+
// This is what you'd have to implement.
98+
await deliverViaMyChannel(messageForBob, receipt.txHash, recipient);
99+
100+
// Recipient: receive the message so it gets processed during sync
101+
await token.methods
102+
.offchain_receive([
103+
{
104+
ciphertext: messageForBob.payload,
105+
recipient: bob,
106+
tx_hash: receipt.txHash.hash,
107+
anchor_block_timestamp: messageForBob.anchorBlockTimestamp,
108+
},
109+
])
110+
.simulate({ from: bob });
118111
```
119112

120-
See the [aztec.js documentation](../../aztec-js/index.md) for more details on accessing transaction effects.
113+
See [Offchain message delivery](./offchain_message_delivery.md) for the full workflow.
121114

122115
### `MessageDelivery::onchain_unconstrained()`
123116

@@ -151,7 +144,7 @@ self.storage.balances.at(recipient).add(amount)
151144
.deliver(MessageDelivery::onchain_constrained());
152145
```
153146

154-
## Choosing a Delivery Mode
147+
## Choosing a delivery mode
155148

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

@@ -204,25 +197,25 @@ MessageDelivery::onchain_unconstrained().via_address_derived_secret()
204197

205198
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.
206199

207-
## Note Discovery and the Sender
200+
## Note discovery and the sender
208201

209202
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.
210203

211-
### Who is the "Sender"?
204+
### Who is the "sender"?
212205

213206
The "sender" for note discovery is **not the contract calling `.deliver()`**. Instead, it's the **account contract** that initiated the transaction.
214207

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

217210
**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.
218211

219-
### Discovering Notes from Unknown Senders
212+
### Discovering notes from unknown senders
220213

221214
When the tag is derived from an address-based shared secret, you cannot compute it for a sender you haven't registered in advance, so you cannot receive those notes from an unknown sender. Handshake protocols let the two parties agree on the secret another way and lift this restriction.
222215

223216
See [You cannot receive address-derived tagged notes from an unknown sender](../../foundational-topics/advanced/storage/note_discovery.md#you-cannot-receive-address-derived-tagged-notes-from-an-unknown-sender) in the note discovery documentation for the approaches and workarounds.
224217

225-
## Delivering to Someone Other Than the Note Owner
218+
## Delivering to someone other than the note owner
226219

227220
You can deliver a note to an address other than the note's owner using `.deliver_to()`:
228221

@@ -239,9 +232,9 @@ self.storage.balances.at(owner).add(amount)
239232
- Game servers that track all note creation and then quickly serve you the game state (results in better UX)
240233
- Analytics or monitoring services
241234

242-
## Code Examples
235+
## Code examples
243236

244-
### Private Token Transfer
237+
### Private token transfer
245238

246239
```rust
247240
#[external("private")]
@@ -258,7 +251,7 @@ fn transfer(amount: u128, sender: AztecAddress, recipient: AztecAddress) {
258251
}
259252
```
260253

261-
### Admin Initialization
254+
### Admin initialization
262255

263256
```rust
264257
#[external("private")]
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
title: Offchain message delivery
3+
tags: [storage, concepts, notes]
4+
description: Deliver notes and events to recipients through your own channel with offchain message delivery, avoiding data availability costs entirely.
5+
sidebar_position: 4
6+
---
7+
8+
Offchain message delivery lets you send notes and events to recipients without posting any data onchain. The encrypted message is returned to the sender's application, which transports it to the recipient through its own channel: a link, a QR code, a direct message, or any other medium. This eliminates data availability (DA) costs and adds zero proving time, at the cost of the application handling transport itself.
9+
10+
This page walks through the complete workflow. For guidance on when offchain delivery is the right choice compared to the onchain modes, see [Note delivery](./note_delivery.md).
11+
12+
## Overview
13+
14+
An offchain-delivered message goes through five stages:
15+
16+
1. **Emission**: The contract delivers a note or event with `MessageDelivery::offchain()`. The message is encrypted to the recipient and emitted as an offchain effect, which is part of the transaction's execution output but never posted onchain.
17+
2. **Extraction**: After sending the transaction, the sender's application reads the emitted messages from the send result (`offchainMessages`).
18+
3. **Transport**: The application delivers each message to its recipient through a channel of its choosing. This step is entirely the application's responsibility.
19+
4. **Receipt**: The recipient hands the message to their wallet by calling the `offchain_receive` utility function on the contract that emitted it. This function is generated automatically by the `#[aztec]` macro, so every Aztec.nr contract has it.
20+
5. **Processing**: The message sits in a local inbox until the transaction that emitted it is found onchain. During private state sync, the wallet decrypts the message, validates the resulting notes and events against onchain data, and adds them to its database.
21+
22+
Because processing validates the message against the onchain transaction, a malformed or dishonest message cannot trick the recipient: a message that cannot be decrypted or validated is simply ignored.
23+
24+
## Emitting offchain messages from a contract
25+
26+
Choose offchain delivery by passing `MessageDelivery::offchain()` to `.deliver()` (for notes) or `.deliver_to()` (for events). The token contract's `transfer_in_private_with_offchain_delivery` function delivers both resulting balance notes and a `Transfer` event offchain:
27+
28+
#include_code transfer_in_private_with_offchain_delivery /noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr rust
29+
30+
Nothing else changes in the contract: the same notes and events are created, only their delivery mechanism differs.
31+
32+
Under the hood, `MessageDelivery::offchain()` encrypts the message without constraints and emits it via the [`deliver_offchain_message`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/offchain_messages/fn.deliver_offchain_message) function. See the [`MessageDelivery`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/message_delivery/struct.MessageDelivery) API reference for details on guarantees, costs, and privacy.
33+
34+
## Extracting messages on the sender side
35+
36+
In `aztec.js`, the result of sending a transaction includes the offchain messages emitted during execution. Each message carries the encrypted `payload`, the `recipient` address, the emitting `contractAddress`, and the `anchorBlockTimestamp` of the transaction:
37+
38+
```typescript
39+
const { receipt, offchainMessages } = await token.methods
40+
.transfer_in_private_with_offchain_delivery(alice, bob, amount, 0)
41+
.send({ from: alice });
42+
43+
// Find the message addressed to the recipient
44+
const messageForBob = offchainMessages.find((msg) => msg.recipient.equals(bob));
45+
```
46+
47+
If you manage proving manually, you can extract the messages from a proven transaction's offchain effects with `extractOffchainOutput`:
48+
49+
```typescript
50+
import { extractOffchainOutput } from "@aztec/aztec.js/contracts";
51+
52+
const { offchainMessages } = extractOffchainOutput(
53+
provenTx.offchainEffects,
54+
provenTx.data.constants.anchorBlockHeader.globalVariables.timestamp,
55+
);
56+
```
57+
58+
## Transporting messages to the recipient
59+
60+
The recipient needs four pieces of information to receive a message:
61+
62+
- the message payload (ciphertext)
63+
- the recipient address
64+
- the hash of the transaction that emitted the message
65+
- the anchor block timestamp
66+
67+
How you get these to the recipient is up to your application. Common patterns include encoding them into a claim link or QR code that the recipient opens, sending them through a messaging service, or storing them where the recipient can fetch them.
68+
69+
:::warning Back up your messages
70+
Offchain messages are not stored onchain, so they cannot be recovered from the network. Until the recipient has processed a message, losing it means the recipient cannot decrypt the corresponding note contents (the note itself still exists onchain).
71+
:::
72+
73+
## Receiving messages
74+
75+
The recipient calls the auto-generated `offchain_receive` utility function on the contract that emitted the message. Utility functions run locally and do not create a transaction, so this call is free:
76+
77+
```typescript
78+
await token.methods
79+
.offchain_receive([
80+
{
81+
ciphertext: messageForBob.payload,
82+
recipient: bob,
83+
tx_hash: receipt.txHash.hash,
84+
anchor_block_timestamp: messageForBob.anchorBlockTimestamp,
85+
},
86+
])
87+
.simulate({ from: bob });
88+
```
89+
90+
`offchain_receive` accepts up to 16 messages per call. It stores them in a persistent inbox scoped to each message's recipient; processing happens later, during sync.
91+
92+
## How messages are processed
93+
94+
Once received, messages are managed by an inbox that the wallet syncs against (see [`receive`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/processing/offchain/fn.receive) in the API reference):
95+
96+
- **Transaction resolution**: A message is only processed once the transaction identified by its `tx_hash` is found onchain. This provides the context needed to validate the notes and events the message contains. Until then, the message waits in the inbox.
97+
- **Reorg safety**: Messages remain in the inbox even after processing. If a reorg reverts the transaction that emitted a message, its effects are rolled back; when the transaction is mined again, the message is reprocessed automatically without needing to be delivered again.
98+
- **Expiry**: Messages are evicted from the inbox once they expire, which happens 26 hours after their anchor block timestamp (the maximum transaction lifetime of 24 hours plus a 2 hour tolerance). A transaction can no longer be included after that point, so an unresolved message can never become processable.
99+
100+
## Current limitations
101+
102+
- **Senders must self-deliver their own messages.** Messages addressed to the sender (such as the change note in a token transfer) are not processed automatically yet. Call `offchain_receive` for them just like you would on the recipient side.
103+
- **Messages must be bound to a transaction.** Messages without an associated transaction hash are not processed in the current version; they sit in the inbox until they expire.
104+
- **Batch size**: A single `offchain_receive` call accepts at most 16 messages. Split larger sets into multiple calls.
105+
106+
## Testing offchain delivery from Noir
107+
108+
The TXE test environment buffers offchain messages emitted during calls and exposes them via `env.offchain_messages()`. Feed them back through `offchain_receive` to complete the delivery loop inside a test:
109+
110+
#include_code token_transfer_offchain_delivery_test /noir-projects/noir-contracts/contracts/app/token_contract/src/test/transfer_in_private_with_offchain_delivery.nr rust
111+
112+
`env.offchain_messages()` returns up to 64 messages, so drain it into batches of at most 16 when a test emits more messages than one `offchain_receive` call accepts.
113+
114+
## Next steps
115+
116+
- Compare delivery modes and their guarantees in [Note delivery](./note_delivery.md)
117+
- Learn how onchain-delivered notes are found in [Note discovery](../../foundational-topics/advanced/storage/note_discovery.md)
118+
- Browse the [`MessageDelivery`](pathname:///aztec-nr-api/#api_ref_version/noir_aztec/messages/message_delivery/struct.MessageDelivery) API reference

noir-projects/aztec-nr/aztec/src/messages/offchain_messages.nr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ pub global OFFCHAIN_MESSAGE_IDENTIFIER: Field =
99
/// Emits a message that will be delivered offchain rather than through the data availability layer.
1010
///
1111
/// Sends data through an alternative app-specific channel without incurring data availability (DA) costs. After
12-
/// receiving the message, the recipient is expected to call the `process_message` function implemented on the contract
13-
/// that originally emitted the message.
12+
/// receiving the message, the recipient is expected to call the `offchain_receive` utility function (injected by the
13+
/// `#[aztec]` macro) on the contract that originally emitted the message.
1414
///
1515
/// # Example use case
1616
///

0 commit comments

Comments
 (0)