@aztec/txe 0.0.1-commit.9badcec54 → 0.0.1-commit.9ebd450e8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
3
3
  import { createLogger } from '@aztec/foundation/log';
4
4
  import { KeyStore } from '@aztec/key-store';
5
5
  import { openTmpStore } from '@aztec/kv-store/lmdb-v2';
6
- import { AddressStore, AnchorBlockStore, CapsuleService, CapsuleStore, ContractSyncService, JobCoordinator, NoteService, NoteStore, PrivateEventStore, RecipientTaggingStore, SenderAddressBookStore, SenderTaggingStore } from '@aztec/pxe/server';
6
+ import { AddressStore, AnchorBlockStore, CapsuleService, CapsuleStore, JobCoordinator, NoteService, NoteStore, PrivateEventStore, RecipientTaggingStore, SenderAddressBookStore, SenderTaggingStore } from '@aztec/pxe/server';
7
7
  import { ExecutionNoteCache, ExecutionTaggingIndexCache, HashedValuesCache, Oracle, PrivateExecutionOracle, UtilityExecutionOracle } from '@aztec/pxe/simulator';
8
8
  import { ExecutionError, WASMSimulator, createSimulationError, extractCallStack, resolveAssertionMessageFromError, toACVMWitness } from '@aztec/simulator/client';
9
9
  import { FunctionSelector, FunctionType } from '@aztec/stdlib/abi';
@@ -11,7 +11,7 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address';
11
11
  import { GasSettings } from '@aztec/stdlib/gas';
12
12
  import { computeProtocolNullifier } from '@aztec/stdlib/hash';
13
13
  import { makeGlobalVariables } from '@aztec/stdlib/testing';
14
- import { CallContext, TxContext } from '@aztec/stdlib/tx';
14
+ import { CallContext, OFFCHAIN_MESSAGE_IDENTIFIER, TxContext } from '@aztec/stdlib/tx';
15
15
  import { z } from 'zod';
16
16
  import { DEFAULT_ADDRESS } from './constants.js';
17
17
  import { TXEOraclePublicContext } from './oracle/txe_oracle_public_context.js';
@@ -22,6 +22,14 @@ import { TXEStateMachine } from './state_machine/index.js';
22
22
  import { TXEAccountStore } from './util/txe_account_store.js';
23
23
  import { getSingleTxBlockRequestHash, insertTxEffectIntoWorldTrees, makeTXEBlock } from './utils/block_creation.js';
24
24
  import { makeTxEffect } from './utils/tx_effect_creation.js';
25
+ function emptyLastCallState() {
26
+ return {
27
+ offchainEffects: [],
28
+ queried: false,
29
+ txHash: Fr.ZERO,
30
+ anchorBlockTimestamp: 0n
31
+ };
32
+ }
25
33
  /**
26
34
  * A `TXESession` corresponds to a Noir `#[test]` function, and handles all of its oracle calls, stores test-specific
27
35
  * state, etc., independent of all other tests running in parallel.
@@ -44,10 +52,10 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
44
52
  chainId;
45
53
  version;
46
54
  nextBlockTimestamp;
47
- contractSyncService;
48
55
  state;
49
56
  authwits;
50
- constructor(logger, stateMachine, oracleHandler, contractStore, noteStore, keyStore, addressStore, accountStore, senderTaggingStore, recipientTaggingStore, senderAddressBookStore, capsuleStore, privateEventStore, jobCoordinator, currentJobId, chainId, version, nextBlockTimestamp, contractSyncService){
57
+ lastCallInfo;
58
+ constructor(logger, stateMachine, oracleHandler, contractStore, noteStore, keyStore, addressStore, accountStore, senderTaggingStore, recipientTaggingStore, senderAddressBookStore, capsuleStore, privateEventStore, jobCoordinator, currentJobId, chainId, version, nextBlockTimestamp){
51
59
  this.logger = logger;
52
60
  this.stateMachine = stateMachine;
53
61
  this.oracleHandler = oracleHandler;
@@ -66,11 +74,11 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
66
74
  this.chainId = chainId;
67
75
  this.version = version;
68
76
  this.nextBlockTimestamp = nextBlockTimestamp;
69
- this.contractSyncService = contractSyncService;
70
77
  this.state = {
71
78
  name: 'TOP_LEVEL'
72
79
  };
73
80
  this.authwits = new Map();
81
+ this.lastCallInfo = emptyLastCallState();
74
82
  }
75
83
  static async init(contractStore) {
76
84
  const store = await openTmpStore('txe-session');
@@ -100,10 +108,9 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
100
108
  const chainId = new Fr(await stateMachine.node.getChainId());
101
109
  const initialJobId = jobCoordinator.beginJob();
102
110
  const logger = createLogger('txe:session');
103
- const contractSyncService = new ContractSyncService(stateMachine.node, contractStore, noteStore, logger);
104
- const topLevelOracleHandler = new TXEOracleTopLevelContext(stateMachine, contractStore, noteStore, keyStore, addressStore, accountStore, senderTaggingStore, recipientTaggingStore, senderAddressBookStore, capsuleStore, privateEventStore, nextBlockTimestamp, version, chainId, new Map(), contractSyncService);
111
+ const topLevelOracleHandler = new TXEOracleTopLevelContext(stateMachine, contractStore, noteStore, keyStore, addressStore, accountStore, senderTaggingStore, recipientTaggingStore, senderAddressBookStore, capsuleStore, privateEventStore, nextBlockTimestamp, version, chainId, new Map());
105
112
  await topLevelOracleHandler.advanceBlocksBy(1);
106
- return new TXESession(logger, stateMachine, topLevelOracleHandler, contractStore, noteStore, keyStore, addressStore, accountStore, senderTaggingStore, recipientTaggingStore, senderAddressBookStore, capsuleStore, privateEventStore, jobCoordinator, initialJobId, version, chainId, nextBlockTimestamp, contractSyncService);
113
+ return new TXESession(logger, stateMachine, topLevelOracleHandler, contractStore, noteStore, keyStore, addressStore, accountStore, senderTaggingStore, recipientTaggingStore, senderAddressBookStore, capsuleStore, privateEventStore, jobCoordinator, initialJobId, version, chainId, nextBlockTimestamp);
107
114
  }
108
115
  /**
109
116
  * Processes an oracle function invoked by the Noir test associated to this session.
@@ -134,6 +141,42 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
134
141
  this.currentJobId = this.jobCoordinator.beginJob();
135
142
  return this.currentJobId;
136
143
  }
144
+ resetLastCall() {
145
+ const notQueriedMessageCount = this.lastCallInfo.queried ? 0 : this.lastCallInfo.offchainEffects.filter((payload)=>payload[0]?.equals(OFFCHAIN_MESSAGE_IDENTIFIER)).length;
146
+ if (notQueriedMessageCount > 0) {
147
+ this.logger.warn(`Dropping ${notQueriedMessageCount} unqueried offchain message(s) from the previous top-level call. ` + `To deliver them, call \`env.offchain_messages()\` and forward the result to the recipient contract's ` + `\`offchain_receive\` utility before issuing another top-level call. To intentionally discard, assign ` + `to \`let _ = env.offchain_messages()\` to silence this warning.`);
148
+ }
149
+ this.lastCallInfo = emptyLastCallState();
150
+ }
151
+ recordOffchainEffect(data) {
152
+ this.lastCallInfo.offchainEffects.push(data);
153
+ }
154
+ setLastCallContext(txHash, anchorBlockTimestamp) {
155
+ this.lastCallInfo.txHash = txHash;
156
+ this.lastCallInfo.anchorBlockTimestamp = anchorBlockTimestamp;
157
+ }
158
+ async withTopLevelCallTracking(work) {
159
+ this.resetLastCall();
160
+ // Capture the anchor *before* `work` runs: private/public executor calls mine a new block as a
161
+ // side effect, and that block's timestamp should not be attributed to this call's anchor.
162
+ const anchorBlockTimestamp = (await this.stateMachine.node.getBlockHeader('latest')).globalVariables.timestamp;
163
+ const { result, txHash } = await work();
164
+ this.setLastCallContext(txHash ?? Fr.ZERO, anchorBlockTimestamp);
165
+ return result;
166
+ }
167
+ getLastCallOffchainEffects() {
168
+ this.lastCallInfo.queried = true;
169
+ return {
170
+ effects: this.lastCallInfo.offchainEffects
171
+ };
172
+ }
173
+ getLastCallContext() {
174
+ const { txHash, anchorBlockTimestamp } = this.lastCallInfo;
175
+ return {
176
+ txHash,
177
+ anchorBlockTimestamp
178
+ };
179
+ }
137
180
  async enterTopLevelState() {
138
181
  switch(this.state.name){
139
182
  case 'PRIVATE':
@@ -162,7 +205,7 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
162
205
  }
163
206
  // Commit all staged stores from the job that was just completed, then begin a new job
164
207
  await this.cycleJob();
165
- this.oracleHandler = new TXEOracleTopLevelContext(this.stateMachine, this.contractStore, this.noteStore, this.keyStore, this.addressStore, this.accountStore, this.senderTaggingStore, this.recipientTaggingStore, this.senderAddressBookStore, this.capsuleStore, this.privateEventStore, this.nextBlockTimestamp, this.version, this.chainId, this.authwits, this.contractSyncService);
208
+ this.oracleHandler = new TXEOracleTopLevelContext(this.stateMachine, this.contractStore, this.noteStore, this.keyStore, this.addressStore, this.accountStore, this.senderTaggingStore, this.recipientTaggingStore, this.senderAddressBookStore, this.capsuleStore, this.privateEventStore, this.nextBlockTimestamp, this.version, this.chainId, this.authwits);
166
209
  this.state = {
167
210
  name: 'TOP_LEVEL'
168
211
  };
@@ -170,6 +213,7 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
170
213
  }
171
214
  async enterPrivateState(contractAddress = DEFAULT_ADDRESS, anchorBlockNumber) {
172
215
  this.exitTopLevelState();
216
+ this.resetLastCall();
173
217
  // Private execution has two associated block numbers: the anchor block (i.e. the historical block that is used to
174
218
  // build the proof), and the *next* block, i.e. the one we'll create once the execution ends, and which will contain
175
219
  // a single transaction with the effects of what was done in the test.
@@ -226,15 +270,19 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
226
270
  taggingIndexCache
227
271
  };
228
272
  this.logger.debug(`Entered state ${this.state.name}`);
273
+ // Record the *resolved* anchor's timestamp — if the caller pinned the anchor to a past block
274
+ // via `anchorBlockNumber`, "latest" would be the wrong anchor for offchain-message semantics.
275
+ this.setLastCallContext(Fr.ZERO, anchorBlock.globalVariables.timestamp);
229
276
  return this.oracleHandler.getPrivateContextInputs();
230
277
  }
231
278
  async enterPublicState(contractAddress) {
232
279
  this.exitTopLevelState();
280
+ this.resetLastCall();
233
281
  // The PublicContext will create a block with a single transaction in it, containing the effects of what was done in
234
282
  // the test. The block therefore gets the *next* block number and timestamp.
235
- const latestBlockNumber = (await this.stateMachine.node.getBlockHeader('latest')).globalVariables.blockNumber;
283
+ const latestHeader = await this.stateMachine.node.getBlockHeader('latest');
236
284
  const globalVariables = makeGlobalVariables(undefined, {
237
- blockNumber: BlockNumber(latestBlockNumber + 1),
285
+ blockNumber: BlockNumber(latestHeader.globalVariables.blockNumber + 1),
238
286
  timestamp: this.nextBlockTimestamp,
239
287
  version: this.version,
240
288
  chainId: this.chainId
@@ -244,9 +292,12 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
244
292
  name: 'PUBLIC'
245
293
  };
246
294
  this.logger.debug(`Entered state ${this.state.name}`);
295
+ // Public state is anchored at the latest block.
296
+ this.setLastCallContext(Fr.ZERO, latestHeader.globalVariables.timestamp);
247
297
  }
248
298
  async enterUtilityState(contractAddress = DEFAULT_ADDRESS) {
249
299
  this.exitTopLevelState();
300
+ this.resetLastCall();
250
301
  const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader();
251
302
  // There is no automatic message discovery and contract-driven syncing process in inlined private or utility
252
303
  // contexts, which means that known nullifiers are also not searched for, since it is during the tagging sync that
@@ -269,7 +320,7 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
269
320
  capsuleService: new CapsuleService(this.capsuleStore, await this.keyStore.getAccounts()),
270
321
  privateEventStore: this.privateEventStore,
271
322
  messageContextService: this.stateMachine.messageContextService,
272
- contractSyncService: this.contractSyncService,
323
+ contractSyncService: this.stateMachine.contractSyncService,
273
324
  l2TipsStore: this.stateMachine.node,
274
325
  jobId: this.currentJobId,
275
326
  scopes: await this.keyStore.getAccounts()
@@ -278,6 +329,8 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
278
329
  name: 'UTILITY'
279
330
  };
280
331
  this.logger.debug(`Entered state ${this.state.name}`);
332
+ // Utility state anchors at whatever the anchor block store is pointing to (tracked as latest).
333
+ this.setLastCallContext(Fr.ZERO, anchorBlockHeader.globalVariables.timestamp);
281
334
  }
282
335
  exitTopLevelState() {
283
336
  if (this.state.name != 'TOP_LEVEL') {
@@ -351,7 +404,7 @@ import { makeTxEffect } from './utils/tx_effect_creation.js';
351
404
  capsuleService: new CapsuleService(this.capsuleStore, scopes),
352
405
  privateEventStore: this.privateEventStore,
353
406
  messageContextService: this.stateMachine.messageContextService,
354
- contractSyncService: this.contractSyncService,
407
+ contractSyncService: this.stateMachine.contractSyncService,
355
408
  l2TipsStore: this.stateMachine.node,
356
409
  jobId: this.currentJobId,
357
410
  scopes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/txe",
3
- "version": "0.0.1-commit.9badcec54",
3
+ "version": "0.0.1-commit.9ebd450e8",
4
4
  "type": "module",
5
5
  "exports": "./dest/index.js",
6
6
  "bin": "./dest/bin/index.js",
@@ -61,20 +61,20 @@
61
61
  ]
62
62
  },
63
63
  "dependencies": {
64
- "@aztec/accounts": "0.0.1-commit.9badcec54",
65
- "@aztec/archiver": "0.0.1-commit.9badcec54",
66
- "@aztec/aztec-node": "0.0.1-commit.9badcec54",
67
- "@aztec/aztec.js": "0.0.1-commit.9badcec54",
68
- "@aztec/bb-prover": "0.0.1-commit.9badcec54",
69
- "@aztec/constants": "0.0.1-commit.9badcec54",
70
- "@aztec/foundation": "0.0.1-commit.9badcec54",
71
- "@aztec/key-store": "0.0.1-commit.9badcec54",
72
- "@aztec/kv-store": "0.0.1-commit.9badcec54",
73
- "@aztec/protocol-contracts": "0.0.1-commit.9badcec54",
74
- "@aztec/pxe": "0.0.1-commit.9badcec54",
75
- "@aztec/simulator": "0.0.1-commit.9badcec54",
76
- "@aztec/stdlib": "0.0.1-commit.9badcec54",
77
- "@aztec/world-state": "0.0.1-commit.9badcec54",
64
+ "@aztec/accounts": "0.0.1-commit.9ebd450e8",
65
+ "@aztec/archiver": "0.0.1-commit.9ebd450e8",
66
+ "@aztec/aztec-node": "0.0.1-commit.9ebd450e8",
67
+ "@aztec/aztec.js": "0.0.1-commit.9ebd450e8",
68
+ "@aztec/bb-prover": "0.0.1-commit.9ebd450e8",
69
+ "@aztec/constants": "0.0.1-commit.9ebd450e8",
70
+ "@aztec/foundation": "0.0.1-commit.9ebd450e8",
71
+ "@aztec/key-store": "0.0.1-commit.9ebd450e8",
72
+ "@aztec/kv-store": "0.0.1-commit.9ebd450e8",
73
+ "@aztec/protocol-contracts": "0.0.1-commit.9ebd450e8",
74
+ "@aztec/pxe": "0.0.1-commit.9ebd450e8",
75
+ "@aztec/simulator": "0.0.1-commit.9ebd450e8",
76
+ "@aztec/stdlib": "0.0.1-commit.9ebd450e8",
77
+ "@aztec/world-state": "0.0.1-commit.9ebd450e8",
78
78
  "zod": "^3.23.8"
79
79
  },
80
80
  "devDependencies": {
@@ -68,7 +68,7 @@ export interface ITxeExecutionOracle {
68
68
  argsHash: Fr,
69
69
  isStaticCall: boolean,
70
70
  jobId: string,
71
- ): Promise<Fr[]>;
71
+ ): Promise<{ returnValues: Fr[]; offchainEffects: Fr[][] }>;
72
72
  executeUtilityFunction(
73
73
  targetContractAddress: AztecAddress,
74
74
  functionSelector: FunctionSelector,
@@ -15,7 +15,6 @@ import {
15
15
  CapsuleService,
16
16
  CapsuleStore,
17
17
  type ContractStore,
18
- type ContractSyncService,
19
18
  NoteStore,
20
19
  ORACLE_VERSION_MAJOR,
21
20
  PrivateEventStore,
@@ -116,7 +115,6 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl
116
115
  private version: Fr,
117
116
  private chainId: Fr,
118
117
  private authwits: Map<string, AuthWitness>,
119
- private readonly contractSyncService: ContractSyncService,
120
118
  ) {
121
119
  this.logger = createLogger('txe:top_level_context');
122
120
  this.logger.debug('Entering Top Level Context');
@@ -514,11 +512,17 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl
514
512
  }
515
513
  }
516
514
 
515
+ // Walk the nested private-call tree and collect every offchain effect the transaction emitted.
516
+ // PXE stores these on each `PrivateCallExecutionResult` and they never reach TXE via the
517
+ // `aztec_utl_emitOffchainEffect` foreign-call path (that path only fires at the top-level), so
518
+ // we pull them out here and the RPC wrapper will hand them to `TXESession` for buffering.
519
+ const offchainEffects = collectNested([executionResult], r => r.offchainEffects.map(e => e.data));
520
+
517
521
  if (isStaticCall) {
518
522
  await checkpoint!.revert();
519
523
 
520
524
  await forkedWorldTrees.close();
521
- return executionResult.returnValues ?? [];
525
+ return { returnValues: executionResult.returnValues ?? [], offchainEffects };
522
526
  }
523
527
 
524
528
  const txEffect = TxEffect.empty();
@@ -540,7 +544,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl
540
544
 
541
545
  await forkedWorldTrees.close();
542
546
 
543
- return executionResult.returnValues ?? [];
547
+ return { returnValues: executionResult.returnValues ?? [], offchainEffects };
544
548
  }
545
549
 
546
550
  async publicCallNewFlow(
@@ -764,7 +768,7 @@ export class TXEOracleTopLevelContext implements IMiscOracle, ITxeExecutionOracl
764
768
  capsuleService: new CapsuleService(this.capsuleStore, scopes),
765
769
  privateEventStore: this.privateEventStore,
766
770
  messageContextService: this.stateMachine.messageContextService,
767
- contractSyncService: this.contractSyncService,
771
+ contractSyncService: this.stateMachine.contractSyncService,
768
772
  l2TipsStore: this.stateMachine.node,
769
773
  jobId,
770
774
  scopes,
@@ -1,6 +1,6 @@
1
1
  import type { ContractInstanceWithAddress } from '@aztec/aztec.js/contracts';
2
2
  import { Fr, Point } from '@aztec/aztec.js/fields';
3
- import { MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX } from '@aztec/constants';
3
+ import { MAX_NOTE_HASHES_PER_TX, MAX_NULLIFIERS_PER_TX, PRIVATE_LOG_CIPHERTEXT_LEN } from '@aztec/constants';
4
4
  import { BlockNumber } from '@aztec/foundation/branded-types';
5
5
  import {
6
6
  type IMiscOracle,
@@ -298,6 +298,46 @@ export class RPCTranslator {
298
298
  ]);
299
299
  }
300
300
 
301
+ // eslint-disable-next-line camelcase
302
+ aztec_txe_getLastCallOffchainEffects() {
303
+ // This oracle returns all offchain effect payloads (messages, authwit requests, etc.) emitted by the last top-level call,
304
+ // MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY is arbitrarily set at 64 because we need a bound. Nothing inherent about it.
305
+ const MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY = 64;
306
+ // Must match MAX_OFFCHAIN_EFFECT_LEN in txe_oracles.nr.
307
+ const MAX_OFFCHAIN_EFFECT_LEN = 2 + PRIVATE_LOG_CIPHERTEXT_LEN;
308
+
309
+ const { effects } = this.stateHandler.getLastCallOffchainEffects();
310
+
311
+ if (effects.length > MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY) {
312
+ throw new Error(`${effects.length} offchain effects exceed max ${MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY}`);
313
+ }
314
+ if (effects.some(e => e.length > MAX_OFFCHAIN_EFFECT_LEN)) {
315
+ throw new Error(`Some offchain effect has length larger than max ${MAX_OFFCHAIN_EFFECT_LEN}`);
316
+ }
317
+
318
+ const rawArrayStorage = effects
319
+ .map(e => e.concat(Array(MAX_OFFCHAIN_EFFECT_LEN - e.length).fill(new Fr(0))))
320
+ .concat(
321
+ Array(MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY - effects.length).fill(Array(MAX_OFFCHAIN_EFFECT_LEN).fill(new Fr(0))),
322
+ )
323
+ .flat();
324
+
325
+ const effectLengths = effects
326
+ .map(e => new Fr(e.length))
327
+ .concat(Array(MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY - effects.length).fill(new Fr(0)));
328
+
329
+ const count = new Fr(effects.length);
330
+
331
+ return toForeignCallResult([toArray(rawArrayStorage), toArray(effectLengths), toSingle(count)]);
332
+ }
333
+
334
+ // eslint-disable-next-line camelcase
335
+ aztec_txe_getLastCallContext() {
336
+ const { txHash, anchorBlockTimestamp } = this.stateHandler.getLastCallContext();
337
+ const isSome = txHash.isZero() ? 0 : 1;
338
+ return toForeignCallResult([toSingle(isSome), toSingle(txHash), toSingle(new Fr(anchorBlockTimestamp))]);
339
+ }
340
+
301
341
  // eslint-disable-next-line camelcase
302
342
  async aztec_txe_getPrivateEvents(
303
343
  foreignSelector: ForeignCallSingle,
@@ -1071,8 +1111,13 @@ export class RPCTranslator {
1071
1111
  }
1072
1112
 
1073
1113
  // eslint-disable-next-line camelcase
1074
- aztec_utl_emitOffchainEffect(_foreignData: ForeignCallArray) {
1075
- throw new Error('Offchain effects are not yet supported in the TestEnvironment');
1114
+ aztec_utl_emitOffchainEffect(foreignData: ForeignCallArray) {
1115
+ // Record the raw payload against the currently-executing top-level call. The Noir side
1116
+ // (via `env.offchain_messages()`) is responsible for decoding the protocol-reserved prefix
1117
+ // (`OFFCHAIN_MESSAGE_IDENTIFIER`, recipient) and turning each payload into an `OffchainMessage` struct suitable
1118
+ // for `offchain_receive`.
1119
+ this.stateHandler.recordOffchainEffect(fromArray(foreignData));
1120
+ return Promise.resolve(toForeignCallResult([]));
1076
1121
  }
1077
1122
 
1078
1123
  // AVM opcodes
@@ -1281,18 +1326,38 @@ export class RPCTranslator {
1281
1326
  const argsHash = fromSingle(foreignArgsHash);
1282
1327
  const isStaticCall = fromSingle(foreignIsStaticCall).toBool();
1283
1328
 
1284
- const returnValues = await this.handlerAsTxe().privateCallNewFlow(
1285
- from,
1286
- targetContractAddress,
1287
- functionSelector,
1288
- args,
1289
- argsHash,
1290
- isStaticCall,
1291
- this.stateHandler.getCurrentJob(),
1292
- );
1329
+ const returnValues = await this.stateHandler.withTopLevelCallTracking(async () => {
1330
+ const { returnValues, offchainEffects } = await this.handlerAsTxe().privateCallNewFlow(
1331
+ from,
1332
+ targetContractAddress,
1333
+ functionSelector,
1334
+ args,
1335
+ argsHash,
1336
+ isStaticCall,
1337
+ this.stateHandler.getCurrentJob(),
1338
+ );
1339
+
1340
+ // Private execution collects offchain effects inside PXE's PrivateExecutionOracle rather than
1341
+ // round-tripping them through `aztec_utl_emitOffchainEffect`, so the session buffer is empty
1342
+ // at this point. Drain the effects from the execution tree into the session buffer so the
1343
+ // next `env.offchain_messages()` call in the test sees them.
1344
+ for (const data of offchainEffects) {
1345
+ this.stateHandler.recordOffchainEffect(data);
1346
+ }
1347
+
1348
+ // TODO(F-335): Avoid doing the following call here.
1349
+ await this.stateHandler.cycleJob();
1350
+
1351
+ if (isStaticCall) {
1352
+ // Static calls revert their checkpoint and mine no block, so there is no tx hash to tag
1353
+ // offchain effects with. Querying `getLastTxEffects()` here would return an unrelated
1354
+ // predecessor tx.
1355
+ return { result: returnValues };
1356
+ }
1357
+ const { txHash } = await this.handlerAsTxe().getLastTxEffects();
1358
+ return { result: returnValues, txHash: txHash.hash };
1359
+ });
1293
1360
 
1294
- // TODO(F-335): Avoid doing the following call here.
1295
- await this.stateHandler.cycleJob();
1296
1361
  return toForeignCallResult([toArray(returnValues)]);
1297
1362
  }
1298
1363
 
@@ -1306,15 +1371,20 @@ export class RPCTranslator {
1306
1371
  const functionSelector = FunctionSelector.fromField(fromSingle(foreignFunctionSelector));
1307
1372
  const args = fromArray(foreignArgs);
1308
1373
 
1309
- const returnValues = await this.handlerAsTxe().executeUtilityFunction(
1310
- targetContractAddress,
1311
- functionSelector,
1312
- args,
1313
- this.stateHandler.getCurrentJob(),
1314
- );
1374
+ const returnValues = await this.stateHandler.withTopLevelCallTracking(async () => {
1375
+ const returnValues = await this.handlerAsTxe().executeUtilityFunction(
1376
+ targetContractAddress,
1377
+ functionSelector,
1378
+ args,
1379
+ this.stateHandler.getCurrentJob(),
1380
+ );
1381
+
1382
+ // TODO(F-335): Avoid doing the following call here.
1383
+ await this.stateHandler.cycleJob();
1384
+
1385
+ return { result: returnValues };
1386
+ });
1315
1387
 
1316
- // TODO(F-335): Avoid doing the following call here.
1317
- await this.stateHandler.cycleJob();
1318
1388
  return toForeignCallResult([toArray(returnValues)]);
1319
1389
  }
1320
1390
 
@@ -1330,10 +1400,20 @@ export class RPCTranslator {
1330
1400
  const calldata = fromArray(foreignCalldata);
1331
1401
  const isStaticCall = fromSingle(foreignIsStaticCall).toBool();
1332
1402
 
1333
- const returnValues = await this.handlerAsTxe().publicCallNewFlow(from, address, calldata, isStaticCall);
1403
+ const returnValues = await this.stateHandler.withTopLevelCallTracking(async () => {
1404
+ const returnValues = await this.handlerAsTxe().publicCallNewFlow(from, address, calldata, isStaticCall);
1405
+
1406
+ // TODO(F-335): Avoid doing the following call here.
1407
+ await this.stateHandler.cycleJob();
1408
+
1409
+ if (isStaticCall) {
1410
+ // See equivalent branch in `aztec_txe_privateCallNewFlow`.
1411
+ return { result: returnValues };
1412
+ }
1413
+ const { txHash } = await this.handlerAsTxe().getLastTxEffects();
1414
+ return { result: returnValues, txHash: txHash.hash };
1415
+ });
1334
1416
 
1335
- // TODO(F-335): Avoid doing the following call here.
1336
- await this.stateHandler.cycleJob();
1337
1417
  return toForeignCallResult([toArray(returnValues)]);
1338
1418
  }
1339
1419