Skip to content

feat: offchain reception#20893

Merged
mverzilli merged 125 commits intomerge-train/fairiesfrom
martin/poc-offchain-reception
Mar 11, 2026
Merged

feat: offchain reception#20893
mverzilli merged 125 commits intomerge-train/fairiesfrom
martin/poc-offchain-reception

Conversation

@mverzilli
Copy link
Contributor

@mverzilli mverzilli commented Feb 26, 2026

Adds the ability to receive offchain messages to Aztec.nr contracts.

It works by using capsules to implement a persistent offchain message inbox, and making sure that inbox is processed whenever sync_state runs.

Apps deliver an offchain message to the inbox through a new utility function offchain_receive, generated by the Aztec macro.

An offchain::sync_inbox function, takes the responsibility to let sync_state know which messages should be processed like so:

  1. Limits processed messages to those whose originating TX are available at PXE's anchor block. To this end it uses a new resolve_message_contexts oracle to determine which messages have corresponding TXs known by PXE.
  2. It is resilient to reorgs. Messages are re-processed for until expiration time to make sure their effects don't get lost after re-orgs. After expiration, they are safe to be discarded.

The feature can be exercised by invoking a function that emits offchain messages on the sender side, saving the resulting offchain effects, and then calling offchain_receive on the recipient side with them.

    // Alice sends the private transfer which emits offchain messages.
    const { receipt, offchainMessages } = await contract.methods
      .transfer_offchain(paymentAmount, bob)
      .send({ from: alice });
    expect(offchainMessages.length).toBeGreaterThan(0);

    const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob));
    expect(messageForBob).toBeTruthy();

    await contract.methods
      .offchain_receive([
        {
          ciphertext: messageForBob!.payload,
          recipient: bob,
          tx_hash: receipt.txHash.hash,
          expiration_timestamp: messageForBob!.expirationTimestamp,
        },
      ])
      .simulate({ from: bob });

Known issues (can be addressed in subsequent PRs):

  1. The new oracle, together with the long window offchain messages get replayed for, means we should really cache getTxEffects node calls to avoid performance regressions.
  2. The current approach of PXE to caching contract sync calls, means that with the current implementation we need to wait for one block before a new message delivered to the inbox gets processed. We will solve this by adding a new oracle to invalidate the contract sync cache.
  3. Scopes aren't handled properly, which might need bigger refactors that exceed offchain

Closes F-323
Closes F-327
Closes F-325

@mverzilli mverzilli requested a review from nventuro March 10, 2026 13:45
Copy link
Contributor

@nventuro nventuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lovely. Left some comments but approving given that this is still in-progress work ultimtaely, no point in holding back the large changeset.

};

let offchain_inbox_sync_option = quote {
Option::some(aztec::messages::processing::offchain::sync_inbox)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So none would be disabling this feature, no?

Copy link
Contributor Author

@mverzilli mverzilli Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, maybe I grew too attached to the first implementation that used extensions

ready_to_process
}

mod test {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With such long tests, you may want to consider putting these in a separate file (which would be processing/offchain/test.nr)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will refactor later


// QR payload is the offchain message for Bob.
const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob));
expect(messageForBob).toBeTruthy();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we leave a comment explaining the capsule issue?

@mverzilli mverzilli enabled auto-merge (squash) March 11, 2026 12:30
@mverzilli mverzilli merged commit b1ada99 into merge-train/fairies Mar 11, 2026
19 checks passed
@mverzilli mverzilli deleted the martin/poc-offchain-reception branch March 11, 2026 12:30
@AztecBot
Copy link
Collaborator

❌ Failed to cherry-pick to v4-next due to conflicts. (🤖) View backport run.

AztecBot pushed a commit that referenced this pull request Mar 11, 2026
AztecBot pushed a commit that referenced this pull request Mar 11, 2026
AztecBot pushed a commit that referenced this pull request Mar 11, 2026
Cherry-pick of b1ada99 onto v4-next.
This commit contains conflict markers for reviewer visibility.
AztecBot pushed a commit that referenced this pull request Mar 11, 2026
Cherry-pick of b1ada99 onto v4-next.
This commit contains conflict markers for reviewer visibility.
benesjan pushed a commit that referenced this pull request Mar 12, 2026
Adds the ability to receive offchain messages to Aztec.nr contracts.

It works by using capsules to implement a persistent offchain message
inbox, and making sure that inbox is processed whenever `sync_state`
runs.

Apps deliver an offchain message to the inbox through a new utility
function `offchain_receive`, generated by the Aztec macro.

An `offchain::sync_inbox` function, takes the responsibility to let
`sync_state` know which messages should be processed like so:

1. Limits processed messages to those whose originating TX are available
at PXE's anchor block. To this end it uses a new
`resolve_message_contexts` oracle to determine which messages have
corresponding TXs known by PXE.
2. It is resilient to reorgs. Messages are re-processed for until
expiration time to make sure their effects don't get lost after re-orgs.
After expiration, they are safe to be discarded.

The feature can be exercised by invoking a function that emits offchain
messages on the sender side, saving the resulting offchain effects, and
then calling `offchain_receive` on the recipient side with them.

```typescript
    // Alice sends the private transfer which emits offchain messages.
    const { receipt, offchainMessages } = await contract.methods
      .transfer_offchain(paymentAmount, bob)
      .send({ from: alice });
    expect(offchainMessages.length).toBeGreaterThan(0);

    const messageForBob = offchainMessages.find(msg => msg.recipient.equals(bob));
    expect(messageForBob).toBeTruthy();

    await contract.methods
      .offchain_receive([
        {
          ciphertext: messageForBob!.payload,
          recipient: bob,
          tx_hash: receipt.txHash.hash,
          expiration_timestamp: messageForBob!.expirationTimestamp,
        },
      ])
      .simulate({ from: bob });
```

Known issues (can be addressed in subsequent PRs):

1. The new oracle, together with the long window offchain messages get
replayed for, means we should really cache `getTxEffects` node calls to
avoid performance regressions.
2. The current approach of PXE to caching contract sync calls, means
that with the current implementation we need to wait for one block
before a new message delivered to the inbox gets processed. We will
solve this by adding a new oracle to invalidate the contract sync cache.
3. Scopes aren't handled properly, which might need bigger refactors
that exceed offchain

Closes F-323
Closes F-327
Closes F-325
nventuro pushed a commit that referenced this pull request Mar 12, 2026
…ization race (#21452)

## Summary
- Sets `anvilSlotsInAnEpoch: 32` in `e2e_offchain_payment` test setup,
matching what `epochs_l1_reorgs` already does.

## Problem
PR #21156 added `--slots-in-an-epoch 1` as the default for anvil, making
`finalized = latest - 2`. PR #20893 added `e2e_offchain_payment` which
simulates L1 reorgs. When both landed on `merge-train/fairies`, the
reorg test fails deterministically because finalization races past the
rollback target block.

## Fix
Use `anvilSlotsInAnEpoch: 32` (matching Ethereum mainnet) so the
finalized block stays far enough behind latest to allow rollbacks in the
test.

ClaudeBox log: https://claudebox.work/s/c5ac5d52da86e23a?run=4
github-merge-queue bot pushed a commit that referenced this pull request Mar 13, 2026
BEGIN_COMMIT_OVERRIDE
fix: skip oracle version check for pinned protocol contracts (#21349)
fix: not reusing tags of partially reverted txs (#20817)
feat: move storage_slot from partial commitment to completion hash
(#21351)
feat: offchain reception (#20893)
fix: handle workspace members in needsRecompile crate collection
(#21284)
fix(aztec-nr): return Option from decode functions and fix event
commitment capacity (#21264)
fix: handle bad note lengths on compute_note_hash_and_nullifier (#21271)
fix: address review feedback from PRs #21284 and #21237 (#21369)
fix: claim contract & improve nullif docs (#21234)
feat!: auto-enqueue public init nullifier for contracts with public
functions (#20775)
fix: search for all note nonces instead of just the one for the note
index (#21438)
fix: set anvilSlotsInAnEpoch in e2e_offchain_payment to prevent
finalization race (#21452)
fix: complete legacy oracle mappings for all pinned contracts (#21404)
fix: correct inverted constrained encryption check in message delivery
(#21399)
feat!: improve L2ToL1MessageWitness API (#21231)
END_COMMIT_OVERRIDE
AztecBot pushed a commit that referenced this pull request Mar 13, 2026
Cherry-pick of b1ada99 onto v4-next.
This commit contains conflict markers for reviewer visibility.
mverzilli added a commit that referenced this pull request Mar 13, 2026
## Summary

Backport of #20893
(feat: offchain reception) to `v4-next`.

This adds offchain message reception to Aztec.nr contracts, including:
- Persistent offchain message inbox via capsules
- `offchain_receive` utility function auto-generated by the Aztec macro
- `offchain::sync_inbox` for processing messages during `sync_state`
- New `utilityResolveMessageContexts` oracle method
- End-to-end test for offchain payments

## Backport details

**Commit 1**: Raw cherry-pick with conflict markers (for reviewer
visibility)
**Commit 2**: Conflict resolution — kept v4-next's `utility`-prefixed
oracle naming convention while integrating new offchain methods
**Commit 3**: Updated `ORACLE_INTERFACE_HASH` for the new oracle method
and bumped `ORACLE_VERSION` from 12 to 13

### Conflicts resolved in:
- `docs/netlify.toml` — added error code 7 redirect
- `noir-projects/aztec-nr/aztec/src/macros/aztec.nr` — integrated
`AztecConfig`, `#[varargs]`, offchain inbox sync
- `noir-projects/aztec-nr/aztec/src/messages/discovery/mod.nr` — added
offchain imports, kept v4-next's generic `Env` type
- `noir-projects/aztec-nr/aztec/src/oracle/version.nr` — bumped to
version 13
-
`yarn-project/pxe/src/contract_function_simulator/oracle/interfaces.ts`
— added `utilityResolveMessageContexts`
- `yarn-project/pxe/src/contract_function_simulator/oracle/oracle.ts` —
added oracle method
-
`yarn-project/pxe/src/contract_function_simulator/oracle/oracle_version_is_checked.test.ts`
— added messageContextService mock
-
`yarn-project/pxe/src/contract_function_simulator/oracle/utility_execution_oracle.ts`
— added implementation
- `yarn-project/pxe/src/oracle_version.ts` — version 13, new hash
- `yarn-project/txe/src/rpc_translator.ts` — added RPC translation

ClaudeBox log: https://claudebox.work/s/f28700a7ecac0c5f?run=1

---------

Co-authored-by: Martin Verzilli <martin@aztec-labs.com>
ludamad added a commit that referenced this pull request Mar 13, 2026
…1414) (#21483)

## Summary

Backport of #21414
to v4-next.

Changes offchain messages to track anchor block timestamp for lifecycle
management instead of transaction TTL. Adds `anchorBlockTimestamp` field
to `OffchainMessage` type and passes it through all code paths that
create offchain messages.

## Conflict Resolution

Rebased onto latest `backport-to-v4-next-staging` (which now includes
the offchain reception backport #20893). Only one conflict remained in
`batch_call.test.ts` — accepted the new test cases from the original PR.

## Verification

- yarn-project builds successfully
- All unit tests pass (`interaction_options.test.ts`,
`batch_call.test.ts`)

ClaudeBox log: https://claudebox.work/s/f59343e7af96e770?run=2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants