@aztec/txe 0.0.1-commit.fff30aa → 0.0.1-dev

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.
@@ -9,7 +9,6 @@ import {
9
9
  CapsuleService,
10
10
  CapsuleStore,
11
11
  ContractStore,
12
- ContractSyncService,
13
12
  JobCoordinator,
14
13
  NoteService,
15
14
  NoteStore,
@@ -39,11 +38,11 @@ import {
39
38
  import { FunctionCall, FunctionSelector, FunctionType } from '@aztec/stdlib/abi';
40
39
  import type { AuthWitness } from '@aztec/stdlib/auth-witness';
41
40
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
42
- import { GasSettings } from '@aztec/stdlib/gas';
41
+ import type { GasSettings } from '@aztec/stdlib/gas';
43
42
  import { computeProtocolNullifier } from '@aztec/stdlib/hash';
44
43
  import { PrivateContextInputs } from '@aztec/stdlib/kernel';
45
44
  import { makeGlobalVariables } from '@aztec/stdlib/testing';
46
- import { CallContext, GlobalVariables, TxContext } from '@aztec/stdlib/tx';
45
+ import { CallContext, GlobalVariables, OFFCHAIN_MESSAGE_IDENTIFIER, TxContext } from '@aztec/stdlib/tx';
47
46
 
48
47
  import { z } from 'zod';
49
48
 
@@ -54,6 +53,7 @@ import { TXEOracleTopLevelContext } from './oracle/txe_oracle_top_level_context.
54
53
  import { RPCTranslator } from './rpc_translator.js';
55
54
  import { TXEArchiver } from './state_machine/archiver.js';
56
55
  import { TXEStateMachine } from './state_machine/index.js';
56
+ import { TXE_ORACLE_VERSION_MAJOR, TXE_ORACLE_VERSION_MINOR } from './txe_oracle_version.js';
57
57
  import type { ForeignCallArgs, ForeignCallResult } from './util/encoding.js';
58
58
  import { TXEAccountStore } from './util/txe_account_store.js';
59
59
  import { getSingleTxBlockRequestHash, insertTxEffectIntoWorldTrees, makeTXEBlock } from './utils/block_creation.js';
@@ -110,14 +110,80 @@ export type TXEOracleFunctionName = Exclude<
110
110
  >;
111
111
 
112
112
  export interface TXESessionStateHandler {
113
+ /** Records the TXE oracle version reported by the Noir test code for diagnostics. */
114
+ setTxeOracleVersion(version: { major: number; minor: number }): void;
115
+
113
116
  enterTopLevelState(): Promise<void>;
114
117
  enterPublicState(contractAddress?: AztecAddress): Promise<void>;
115
- enterPrivateState(contractAddress?: AztecAddress, anchorBlockNumber?: BlockNumber): Promise<PrivateContextInputs>;
118
+ enterPrivateState(
119
+ contractAddress: AztecAddress | undefined,
120
+ anchorBlockNumber: BlockNumber | undefined,
121
+ gasSettings: GasSettings,
122
+ ): Promise<PrivateContextInputs>;
116
123
  enterUtilityState(contractAddress?: AztecAddress): Promise<void>;
117
124
 
118
125
  // TODO(F-335): Exposing the job info is abstraction breakage - drop the following 2 functions.
119
126
  cycleJob(): Promise<string>;
120
127
  getCurrentJob(): string;
128
+
129
+ /**
130
+ * Runs an executor-style top-level call (private/public call, utility execution) with last-call tracking.
131
+ */
132
+ withTopLevelCallTracking<T>(work: () => Promise<{ result: T; txHash?: Fr }>): Promise<T>;
133
+
134
+ /**
135
+ * Captures a raw offchain effect payload for consumption from test environment. Called by the `emit_offchain_effect`
136
+ * oracle handler whenever a contract function emits an offchain message, at any call depth.
137
+ */
138
+ recordOffchainEffect(data: Fr[]): void;
139
+
140
+ /**
141
+ * Returns the raw offchain effect payloads emitted by the last top-level call. Each payload follows the protocol
142
+ * convention documented on `OFFCHAIN_MESSAGE_IDENTIFIER`, i.e. `[identifier, recipient, ...ciphertext]`. Decoding into
143
+ * `OffchainMessage` structs happens on the Noir side of the test helper. Marks the buffer as queried so the
144
+ * unqueried-messages warning doesn't fire on the next reset.
145
+ */
146
+ getLastCallOffchainEffects(): { effects: Fr[][] };
147
+
148
+ /**
149
+ * Returns the context of the last top-level call: its tx hash (`Fr.ZERO` if the call was tx-less) and the anchor
150
+ * block timestamp captured at the start of the call. Does *not* mark the buffer as queried — context reads are
151
+ * metadata, not effect consumption.
152
+ */
153
+ getLastCallContext(): { txHash: Fr; anchorBlockTimestamp: bigint };
154
+ }
155
+
156
+ /**
157
+ * Session state tracking the most recently completed top-level call: the offchain effect buffer it produced, and the
158
+ * call's context (tx hash + anchor block timestamp). The context is refreshed on every top-level call, independently
159
+ * of whether the call produced offchain effects.
160
+ */
161
+ interface LastCallState {
162
+ /**
163
+ * Raw offchain effect payloads emitted by the currently-executing (or most recently completed) top-level call. Wiped
164
+ * at the start of every top-level entry point, appended to on every `emit_offchain_effect` oracle invocation.
165
+ */
166
+ offchainEffects: Fr[][];
167
+ /**
168
+ * Tracks whether the test has queried `effects` since the last reset. If a new top-level call clobbers the buffer
169
+ * without it being queried first, any accumulated messages are lost and we emit a warning so tests don't silently
170
+ * drop delivery.
171
+ */
172
+ queried: boolean;
173
+ /**
174
+ * Tx hash of the most recently completed top-level call, or `Fr.ZERO` if the call was tx-less (context setters,
175
+ * utility execution). Populated by call executor handlers after execution completes.
176
+ */
177
+ txHash: Fr;
178
+ /**
179
+ * Anchor block timestamp of the most recently completed top-level call, captured from the anchor block header that
180
+ * was active when the call started. Populated by call executor handlers after execution completes.
181
+ */
182
+ anchorBlockTimestamp: bigint;
183
+ }
184
+
185
+ function emptyLastCallState(): LastCallState {
186
+ return { offchainEffects: [], queried: false, txHash: Fr.ZERO, anchorBlockTimestamp: 0n };
121
187
  }
122
188
 
123
189
  /**
@@ -127,6 +193,8 @@ export interface TXESessionStateHandler {
127
193
  export class TXESession implements TXESessionStateHandler {
128
194
  private state: SessionState = { name: 'TOP_LEVEL' };
129
195
  private authwits: Map<string, AuthWitness> = new Map();
196
+ private lastCallInfo: LastCallState = emptyLastCallState();
197
+ private txeOracleVersion: { major: number; minor: number } | undefined;
130
198
 
131
199
  constructor(
132
200
  private logger: Logger,
@@ -151,7 +219,6 @@ export class TXESession implements TXESessionStateHandler {
151
219
  private chainId: Fr,
152
220
  private version: Fr,
153
221
  private nextBlockTimestamp: bigint,
154
- private contractSyncService: ContractSyncService,
155
222
  ) {}
156
223
 
157
224
  static async init(contractStore: ContractStore) {
@@ -188,7 +255,6 @@ export class TXESession implements TXESessionStateHandler {
188
255
  const initialJobId = jobCoordinator.beginJob();
189
256
 
190
257
  const logger = createLogger('txe:session');
191
- const contractSyncService = new ContractSyncService(stateMachine.node, contractStore, noteStore, logger);
192
258
 
193
259
  const topLevelOracleHandler = new TXEOracleTopLevelContext(
194
260
  stateMachine,
@@ -206,7 +272,6 @@ export class TXESession implements TXESessionStateHandler {
206
272
  version,
207
273
  chainId,
208
274
  new Map(),
209
- contractSyncService,
210
275
  );
211
276
  await topLevelOracleHandler.advanceBlocksBy(1);
212
277
 
@@ -229,7 +294,6 @@ export class TXESession implements TXESessionStateHandler {
229
294
  version,
230
295
  chainId,
231
296
  nextBlockTimestamp,
232
- contractSyncService,
233
297
  );
234
298
  }
235
299
 
@@ -251,7 +315,28 @@ export class TXESession implements TXESessionStateHandler {
251
315
  return translator[validatedFunctionName](...inputs);
252
316
  } catch (error) {
253
317
  if (error instanceof z.ZodError) {
254
- throw new Error(`${functionName} does not correspond to any oracle handler available on RPCTranslator`);
318
+ let versionHint: string;
319
+ if (!this.txeOracleVersion) {
320
+ versionHint =
321
+ ' The test appears to use an older version of Aztec.nr that does not' +
322
+ ' support test environment oracle versioning. Update Aztec.nr to a compatible version.' +
323
+ ' See https://docs.aztec.network/errors/12';
324
+ } else if (this.txeOracleVersion.minor > TXE_ORACLE_VERSION_MINOR) {
325
+ versionHint =
326
+ ` The test uses Aztec.nr test oracle version` +
327
+ ` ${this.txeOracleVersion.major}.${this.txeOracleVersion.minor}, but this test environment` +
328
+ ` only supports up to ${TXE_ORACLE_VERSION_MAJOR}.${TXE_ORACLE_VERSION_MINOR}.` +
329
+ ` Upgrade the Aztec CLI to a compatible version.` +
330
+ ` See https://docs.aztec.network/errors/12`;
331
+ } else {
332
+ versionHint =
333
+ ` The test's oracle version (${this.txeOracleVersion.major}.${this.txeOracleVersion.minor})` +
334
+ ` is compatible with this test environment` +
335
+ ` (${TXE_ORACLE_VERSION_MAJOR}.${TXE_ORACLE_VERSION_MINOR}), so this oracle should be` +
336
+ ` available. This is an unexpected error, please report it.` +
337
+ ` See https://docs.aztec.network/errors/13`;
338
+ }
339
+ throw new Error(`Unknown oracle '${functionName}'.${versionHint}`);
255
340
  } else if (error instanceof Error) {
256
341
  throw new Error(
257
342
  `Execution error while processing function ${functionName} in state ${this.state.name}: ${error.message}`,
@@ -275,6 +360,55 @@ export class TXESession implements TXESessionStateHandler {
275
360
  return this.currentJobId;
276
361
  }
277
362
 
363
+ private resetLastCall(): void {
364
+ const notQueriedMessageCount = this.lastCallInfo.queried
365
+ ? 0
366
+ : this.lastCallInfo.offchainEffects.filter(payload => payload[0]?.equals(OFFCHAIN_MESSAGE_IDENTIFIER)).length;
367
+ if (notQueriedMessageCount > 0) {
368
+ this.logger.warn(
369
+ `Dropping ${notQueriedMessageCount} unqueried offchain message(s) from the previous top-level call. ` +
370
+ `To deliver them, call \`env.offchain_messages()\` and forward the result to the recipient contract's ` +
371
+ `\`offchain_receive\` utility before issuing another top-level call. To intentionally discard, assign ` +
372
+ `to \`let _ = env.offchain_messages()\` to silence this warning.`,
373
+ );
374
+ }
375
+ this.lastCallInfo = emptyLastCallState();
376
+ }
377
+
378
+ recordOffchainEffect(data: Fr[]): void {
379
+ this.lastCallInfo.offchainEffects.push(data);
380
+ }
381
+
382
+ private setLastCallContext(txHash: Fr, anchorBlockTimestamp: bigint): void {
383
+ this.lastCallInfo.txHash = txHash;
384
+ this.lastCallInfo.anchorBlockTimestamp = anchorBlockTimestamp;
385
+ }
386
+
387
+ async withTopLevelCallTracking<T>(work: () => Promise<{ result: T; txHash?: Fr }>): Promise<T> {
388
+ this.resetLastCall();
389
+ // Capture the anchor *before* `work` runs: private/public executor calls mine a new block as a
390
+ // side effect, and that block's timestamp should not be attributed to this call's anchor.
391
+ const anchorBlockTimestamp = (await this.stateMachine.node.getBlockHeader('latest'))!.globalVariables.timestamp;
392
+ const { result, txHash } = await work();
393
+ this.setLastCallContext(txHash ?? Fr.ZERO, anchorBlockTimestamp);
394
+ return result;
395
+ }
396
+
397
+ getLastCallOffchainEffects(): { effects: Fr[][] } {
398
+ this.lastCallInfo.queried = true;
399
+ return { effects: this.lastCallInfo.offchainEffects };
400
+ }
401
+
402
+ getLastCallContext(): { txHash: Fr; anchorBlockTimestamp: bigint } {
403
+ const { txHash, anchorBlockTimestamp } = this.lastCallInfo;
404
+ return { txHash, anchorBlockTimestamp };
405
+ }
406
+
407
+ setTxeOracleVersion(version: { major: number; minor: number }): void {
408
+ this.txeOracleVersion = version;
409
+ this.logger.debug(`Test compiled with test oracle version ${version.major}.${version.minor}`);
410
+ }
411
+
278
412
  async enterTopLevelState() {
279
413
  switch (this.state.name) {
280
414
  case 'PRIVATE': {
@@ -316,7 +450,6 @@ export class TXESession implements TXESessionStateHandler {
316
450
  this.version,
317
451
  this.chainId,
318
452
  this.authwits,
319
- this.contractSyncService,
320
453
  );
321
454
 
322
455
  this.state = { name: 'TOP_LEVEL' };
@@ -325,9 +458,11 @@ export class TXESession implements TXESessionStateHandler {
325
458
 
326
459
  async enterPrivateState(
327
460
  contractAddress: AztecAddress = DEFAULT_ADDRESS,
328
- anchorBlockNumber?: BlockNumber,
461
+ anchorBlockNumber: BlockNumber | undefined,
462
+ gasSettings: GasSettings,
329
463
  ): Promise<PrivateContextInputs> {
330
464
  this.exitTopLevelState();
465
+ this.resetLastCall();
331
466
 
332
467
  // Private execution has two associated block numbers: the anchor block (i.e. the historical block that is used to
333
468
  // build the proof), and the *next* block, i.e. the one we'll create once the execution ends, and which will contain
@@ -355,7 +490,7 @@ export class TXESession implements TXESessionStateHandler {
355
490
  const utilityExecutor = this.utilityExecutorForContractSync(anchorBlock);
356
491
  this.oracleHandler = new PrivateExecutionOracle({
357
492
  argsHash: Fr.ZERO,
358
- txContext: new TxContext(this.chainId, this.version, GasSettings.empty()),
493
+ txContext: new TxContext(this.chainId, this.version, gasSettings),
359
494
  callContext: new CallContext(AztecAddress.ZERO, contractAddress, FunctionSelector.empty(), false),
360
495
  anchorBlockHeader: anchorBlock!,
361
496
  utilityExecutor,
@@ -379,6 +514,7 @@ export class TXESession implements TXESessionStateHandler {
379
514
  jobId: this.currentJobId,
380
515
  scopes: await this.keyStore.getAccounts(),
381
516
  messageContextService: this.stateMachine.messageContextService,
517
+ simulator: new WASMSimulator(),
382
518
  });
383
519
 
384
520
  // We store the note and tagging index caches fed into the PrivateExecutionOracle (along with some other auxiliary
@@ -389,17 +525,22 @@ export class TXESession implements TXESessionStateHandler {
389
525
  this.state = { name: 'PRIVATE', nextBlockGlobalVariables, noteCache, taggingIndexCache };
390
526
  this.logger.debug(`Entered state ${this.state.name}`);
391
527
 
528
+ // Record the *resolved* anchor's timestamp — if the caller pinned the anchor to a past block
529
+ // via `anchorBlockNumber`, "latest" would be the wrong anchor for offchain-message semantics.
530
+ this.setLastCallContext(Fr.ZERO, anchorBlock!.globalVariables.timestamp);
531
+
392
532
  return (this.oracleHandler as PrivateExecutionOracle).getPrivateContextInputs();
393
533
  }
394
534
 
395
535
  async enterPublicState(contractAddress?: AztecAddress) {
396
536
  this.exitTopLevelState();
537
+ this.resetLastCall();
397
538
 
398
539
  // The PublicContext will create a block with a single transaction in it, containing the effects of what was done in
399
540
  // the test. The block therefore gets the *next* block number and timestamp.
400
- const latestBlockNumber = (await this.stateMachine.node.getBlockHeader('latest'))!.globalVariables.blockNumber;
541
+ const latestHeader = (await this.stateMachine.node.getBlockHeader('latest'))!;
401
542
  const globalVariables = makeGlobalVariables(undefined, {
402
- blockNumber: BlockNumber(latestBlockNumber + 1),
543
+ blockNumber: BlockNumber(latestHeader.globalVariables.blockNumber + 1),
403
544
  timestamp: this.nextBlockTimestamp,
404
545
  version: this.version,
405
546
  chainId: this.chainId,
@@ -414,10 +555,14 @@ export class TXESession implements TXESessionStateHandler {
414
555
 
415
556
  this.state = { name: 'PUBLIC' };
416
557
  this.logger.debug(`Entered state ${this.state.name}`);
558
+
559
+ // Public state is anchored at the latest block.
560
+ this.setLastCallContext(Fr.ZERO, latestHeader.globalVariables.timestamp);
417
561
  }
418
562
 
419
563
  async enterUtilityState(contractAddress: AztecAddress = DEFAULT_ADDRESS) {
420
564
  this.exitTopLevelState();
565
+ this.resetLastCall();
421
566
 
422
567
  const anchorBlockHeader = await this.stateMachine.anchorBlockStore.getBlockHeader();
423
568
 
@@ -448,14 +593,19 @@ export class TXESession implements TXESessionStateHandler {
448
593
  capsuleService: new CapsuleService(this.capsuleStore, await this.keyStore.getAccounts()),
449
594
  privateEventStore: this.privateEventStore,
450
595
  messageContextService: this.stateMachine.messageContextService,
451
- contractSyncService: this.contractSyncService,
596
+ contractSyncService: this.stateMachine.contractSyncService,
452
597
  l2TipsStore: this.stateMachine.node,
453
598
  jobId: this.currentJobId,
454
599
  scopes: await this.keyStore.getAccounts(),
600
+ simulator: new WASMSimulator(),
601
+ utilityExecutor: this.utilityExecutorForContractSync(anchorBlockHeader),
455
602
  });
456
603
 
457
604
  this.state = { name: 'UTILITY' };
458
605
  this.logger.debug(`Entered state ${this.state.name}`);
606
+
607
+ // Utility state anchors at whatever the anchor block store is pointing to (tracked as latest).
608
+ this.setLastCallContext(Fr.ZERO, anchorBlockHeader.globalVariables.timestamp);
459
609
  }
460
610
 
461
611
  private exitTopLevelState() {
@@ -527,6 +677,7 @@ export class TXESession implements TXESessionStateHandler {
527
677
  }
528
678
 
529
679
  try {
680
+ const simulator = new WASMSimulator();
530
681
  const oracle = new UtilityExecutionOracle({
531
682
  contractAddress: call.to,
532
683
  authWitnesses: [],
@@ -542,12 +693,14 @@ export class TXESession implements TXESessionStateHandler {
542
693
  capsuleService: new CapsuleService(this.capsuleStore, scopes),
543
694
  privateEventStore: this.privateEventStore,
544
695
  messageContextService: this.stateMachine.messageContextService,
545
- contractSyncService: this.contractSyncService,
696
+ contractSyncService: this.stateMachine.contractSyncService,
546
697
  l2TipsStore: this.stateMachine.node,
547
698
  jobId: this.currentJobId,
548
699
  scopes,
700
+ simulator,
701
+ utilityExecutor: this.utilityExecutorForContractSync(anchorBlock),
549
702
  });
550
- await new WASMSimulator()
703
+ await simulator
551
704
  .executeUserCircuit(toACVMWitness(0, call.args), entryPointArtifact, new Oracle(oracle).toACIRCallback())
552
705
  .catch((err: Error) => {
553
706
  err.message = resolveAssertionMessageFromError(err, entryPointArtifact);