@aztec/pxe 0.23.0 → 0.26.1

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.
Files changed (76) hide show
  1. package/dest/config/index.js +2 -2
  2. package/dest/database/deferred_note_dao.d.ts +8 -4
  3. package/dest/database/deferred_note_dao.d.ts.map +1 -1
  4. package/dest/database/deferred_note_dao.js +9 -6
  5. package/dest/database/note_dao.d.ts +4 -0
  6. package/dest/database/note_dao.d.ts.map +1 -1
  7. package/dest/database/note_dao.js +7 -2
  8. package/dest/database/pxe_database_test_suite.js +5 -5
  9. package/dest/kernel_oracle/index.d.ts +1 -0
  10. package/dest/kernel_oracle/index.d.ts.map +1 -1
  11. package/dest/kernel_oracle/index.js +4 -1
  12. package/dest/kernel_prover/hints_builder.d.ts +36 -0
  13. package/dest/kernel_prover/hints_builder.d.ts.map +1 -0
  14. package/dest/kernel_prover/hints_builder.js +115 -0
  15. package/dest/kernel_prover/kernel_prover.d.ts +2 -24
  16. package/dest/kernel_prover/kernel_prover.d.ts.map +1 -1
  17. package/dest/kernel_prover/kernel_prover.js +32 -103
  18. package/dest/kernel_prover/proof_creator.d.ts +11 -11
  19. package/dest/kernel_prover/proof_creator.d.ts.map +1 -1
  20. package/dest/kernel_prover/proof_creator.js +6 -6
  21. package/dest/kernel_prover/proving_data_oracle.d.ts +2 -0
  22. package/dest/kernel_prover/proving_data_oracle.d.ts.map +1 -1
  23. package/dest/note_processor/note_processor.d.ts.map +1 -1
  24. package/dest/note_processor/note_processor.js +15 -14
  25. package/dest/note_processor/produce_note_dao.d.ts +2 -2
  26. package/dest/note_processor/produce_note_dao.d.ts.map +1 -1
  27. package/dest/note_processor/produce_note_dao.js +8 -8
  28. package/dest/pxe_http/pxe_http_server.d.ts.map +1 -1
  29. package/dest/pxe_http/pxe_http_server.js +7 -6
  30. package/dest/pxe_service/create_pxe_service.d.ts.map +1 -1
  31. package/dest/pxe_service/create_pxe_service.js +5 -1
  32. package/dest/pxe_service/pxe_service.d.ts +5 -3
  33. package/dest/pxe_service/pxe_service.d.ts.map +1 -1
  34. package/dest/pxe_service/pxe_service.js +51 -44
  35. package/dest/pxe_service/test/pxe_test_suite.d.ts.map +1 -1
  36. package/dest/pxe_service/test/pxe_test_suite.js +3 -3
  37. package/dest/simulator_oracle/index.d.ts +7 -4
  38. package/dest/simulator_oracle/index.d.ts.map +1 -1
  39. package/dest/simulator_oracle/index.js +17 -8
  40. package/dest/synchronizer/synchronizer.d.ts.map +1 -1
  41. package/dest/synchronizer/synchronizer.js +14 -42
  42. package/package.json +14 -13
  43. package/src/bin/index.ts +42 -0
  44. package/src/config/index.ts +40 -0
  45. package/src/contract_data_oracle/index.ts +185 -0
  46. package/src/contract_data_oracle/private_functions_tree.ts +127 -0
  47. package/src/contract_database/index.ts +1 -0
  48. package/src/contract_database/memory_contract_database.ts +58 -0
  49. package/src/database/contracts/contract_artifact_db.ts +19 -0
  50. package/src/database/contracts/contract_instance_db.ts +18 -0
  51. package/src/database/deferred_note_dao.ts +55 -0
  52. package/src/database/index.ts +1 -0
  53. package/src/database/kv_pxe_database.ts +409 -0
  54. package/src/database/note_dao.ts +97 -0
  55. package/src/database/pxe_database.ts +162 -0
  56. package/src/database/pxe_database_test_suite.ts +258 -0
  57. package/src/index.ts +11 -0
  58. package/src/kernel_oracle/index.ts +65 -0
  59. package/src/kernel_prover/hints_builder.ts +170 -0
  60. package/src/kernel_prover/index.ts +2 -0
  61. package/src/kernel_prover/kernel_prover.ts +309 -0
  62. package/src/kernel_prover/proof_creator.ts +157 -0
  63. package/src/kernel_prover/proving_data_oracle.ts +79 -0
  64. package/src/note_processor/index.ts +1 -0
  65. package/src/note_processor/note_processor.ts +275 -0
  66. package/src/note_processor/produce_note_dao.ts +132 -0
  67. package/src/pxe_http/index.ts +1 -0
  68. package/src/pxe_http/pxe_http_server.ts +75 -0
  69. package/src/pxe_service/create_pxe_service.ts +53 -0
  70. package/src/pxe_service/index.ts +3 -0
  71. package/src/pxe_service/pxe_service.ts +769 -0
  72. package/src/pxe_service/test/pxe_test_suite.ts +140 -0
  73. package/src/simulator/index.ts +24 -0
  74. package/src/simulator_oracle/index.ts +222 -0
  75. package/src/synchronizer/index.ts +1 -0
  76. package/src/synchronizer/synchronizer.ts +385 -0
@@ -0,0 +1,769 @@
1
+ import {
2
+ AuthWitness,
3
+ AztecNode,
4
+ ContractDao,
5
+ ContractData,
6
+ DeployedContract,
7
+ ExtendedContractData,
8
+ ExtendedNote,
9
+ FunctionCall,
10
+ GetUnencryptedLogsResponse,
11
+ KeyStore,
12
+ L2Block,
13
+ LogFilter,
14
+ MerkleTreeId,
15
+ NoteFilter,
16
+ PXE,
17
+ SimulationError,
18
+ Tx,
19
+ TxEffect,
20
+ TxExecutionRequest,
21
+ TxHash,
22
+ TxL2Logs,
23
+ TxReceipt,
24
+ getNewContractPublicFunctions,
25
+ isNoirCallStackUnresolved,
26
+ } from '@aztec/circuit-types';
27
+ import { TxPXEProcessingStats } from '@aztec/circuit-types/stats';
28
+ import {
29
+ AztecAddress,
30
+ CallRequest,
31
+ CompleteAddress,
32
+ FunctionData,
33
+ GrumpkinPrivateKey,
34
+ MAX_NON_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
35
+ MAX_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
36
+ PartialAddress,
37
+ PrivateKernelTailCircuitPublicInputs,
38
+ PublicCallRequest,
39
+ computeArtifactHash,
40
+ computeContractClassId,
41
+ computeSaltedInitializationHash,
42
+ getContractClassFromArtifact,
43
+ } from '@aztec/circuits.js';
44
+ import { computeCommitmentNonce, siloNullifier } from '@aztec/circuits.js/hash';
45
+ import { DecodedReturn, encodeArguments } from '@aztec/foundation/abi';
46
+ import { arrayNonEmptyLength, padArrayEnd } from '@aztec/foundation/collection';
47
+ import { Fr } from '@aztec/foundation/fields';
48
+ import { SerialQueue } from '@aztec/foundation/fifo';
49
+ import { DebugLogger, createDebugLogger } from '@aztec/foundation/log';
50
+ import { Timer } from '@aztec/foundation/timer';
51
+ import {
52
+ AcirSimulator,
53
+ ExecutionResult,
54
+ collectEncryptedLogs,
55
+ collectEnqueuedPublicFunctionCalls,
56
+ collectUnencryptedLogs,
57
+ resolveOpcodeLocations,
58
+ } from '@aztec/simulator';
59
+ import { ContractClassWithId, ContractInstanceWithAddress } from '@aztec/types/contracts';
60
+ import { NodeInfo } from '@aztec/types/interfaces';
61
+
62
+ import { PXEServiceConfig, getPackageInfo } from '../config/index.js';
63
+ import { ContractDataOracle } from '../contract_data_oracle/index.js';
64
+ import { PxeDatabase } from '../database/index.js';
65
+ import { NoteDao } from '../database/note_dao.js';
66
+ import { KernelOracle } from '../kernel_oracle/index.js';
67
+ import { KernelProver } from '../kernel_prover/kernel_prover.js';
68
+ import { getAcirSimulator } from '../simulator/index.js';
69
+ import { Synchronizer } from '../synchronizer/index.js';
70
+
71
+ /**
72
+ * A Private eXecution Environment (PXE) implementation.
73
+ */
74
+ export class PXEService implements PXE {
75
+ private synchronizer: Synchronizer;
76
+ private contractDataOracle: ContractDataOracle;
77
+ private simulator: AcirSimulator;
78
+ private log: DebugLogger;
79
+ private nodeVersion: string;
80
+ // serialize synchronizer and calls to simulateTx.
81
+ // ensures that state is not changed while simulating
82
+ private jobQueue = new SerialQueue();
83
+
84
+ constructor(
85
+ private keyStore: KeyStore,
86
+ private node: AztecNode,
87
+ private db: PxeDatabase,
88
+ private config: PXEServiceConfig,
89
+ logSuffix?: string,
90
+ ) {
91
+ this.log = createDebugLogger(logSuffix ? `aztec:pxe_service_${logSuffix}` : `aztec:pxe_service`);
92
+ this.synchronizer = new Synchronizer(node, db, this.jobQueue, logSuffix);
93
+ this.contractDataOracle = new ContractDataOracle(db);
94
+ this.simulator = getAcirSimulator(db, node, keyStore, this.contractDataOracle);
95
+ this.nodeVersion = getPackageInfo().version;
96
+
97
+ this.jobQueue.start();
98
+ }
99
+
100
+ /**
101
+ * Starts the PXE Service by beginning the synchronization process between the Aztec node and the database.
102
+ *
103
+ * @returns A promise that resolves when the server has started successfully.
104
+ */
105
+ public async start() {
106
+ const { l2BlockPollingIntervalMS } = this.config;
107
+ await this.synchronizer.start(1, l2BlockPollingIntervalMS);
108
+ await this.restoreNoteProcessors();
109
+ const info = await this.getNodeInfo();
110
+ this.log.info(`Started PXE connected to chain ${info.chainId} version ${info.protocolVersion}`);
111
+ }
112
+
113
+ private async restoreNoteProcessors() {
114
+ const publicKeys = await this.keyStore.getAccounts();
115
+ const publicKeysSet = new Set(publicKeys.map(k => k.toString()));
116
+
117
+ const registeredAddresses = await this.db.getCompleteAddresses();
118
+
119
+ let count = 0;
120
+ for (const address of registeredAddresses) {
121
+ if (!publicKeysSet.has(address.publicKey.toString())) {
122
+ continue;
123
+ }
124
+
125
+ count++;
126
+ this.synchronizer.addAccount(address.publicKey, this.keyStore, this.config.l2StartingBlock);
127
+ }
128
+
129
+ if (count > 0) {
130
+ this.log(`Restored ${count} accounts`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Stops the PXE Service, halting processing of new transactions and shutting down the synchronizer.
136
+ * This function ensures that all ongoing tasks are completed before stopping the server.
137
+ * It is useful for gracefully shutting down the server during maintenance or restarts.
138
+ *
139
+ * @returns A Promise resolving once the server has been stopped successfully.
140
+ */
141
+ public async stop() {
142
+ await this.jobQueue.cancel();
143
+ this.log.info('Cancelled Job Queue');
144
+ await this.synchronizer.stop();
145
+ this.log.info('Stopped Synchronizer');
146
+ }
147
+
148
+ /** Returns an estimate of the db size in bytes. */
149
+ public estimateDbSize() {
150
+ return this.db.estimateSize();
151
+ }
152
+
153
+ public addAuthWitness(witness: AuthWitness) {
154
+ return this.db.addAuthWitness(witness.requestHash, witness.witness);
155
+ }
156
+
157
+ public addCapsule(capsule: Fr[]) {
158
+ return this.db.addCapsule(capsule);
159
+ }
160
+
161
+ public getContractInstance(address: AztecAddress): Promise<ContractInstanceWithAddress | undefined> {
162
+ return this.db.getContractInstance(address);
163
+ }
164
+
165
+ public async getContractClass(id: Fr): Promise<ContractClassWithId | undefined> {
166
+ const artifact = await this.db.getContractArtifact(id);
167
+ return artifact && getContractClassFromArtifact(artifact);
168
+ }
169
+
170
+ public async registerAccount(privKey: GrumpkinPrivateKey, partialAddress: PartialAddress): Promise<CompleteAddress> {
171
+ const completeAddress = CompleteAddress.fromPrivateKeyAndPartialAddress(privKey, partialAddress);
172
+ const wasAdded = await this.db.addCompleteAddress(completeAddress);
173
+ if (wasAdded) {
174
+ const pubKey = await this.keyStore.addAccount(privKey);
175
+ this.synchronizer.addAccount(pubKey, this.keyStore, this.config.l2StartingBlock);
176
+ this.log.info(`Registered account ${completeAddress.address.toString()}`);
177
+ this.log.debug(`Registered account\n ${completeAddress.toReadableString()}`);
178
+ } else {
179
+ this.log.info(`Account:\n "${completeAddress.address.toString()}"\n already registered.`);
180
+ }
181
+ return completeAddress;
182
+ }
183
+
184
+ public async getRegisteredAccounts(): Promise<CompleteAddress[]> {
185
+ // Get complete addresses of both the recipients and the accounts
186
+ const addresses = await this.db.getCompleteAddresses();
187
+ // Filter out the addresses not corresponding to accounts
188
+ const accountPubKeys = await this.keyStore.getAccounts();
189
+ const accounts = addresses.filter(address => accountPubKeys.find(pubKey => pubKey.equals(address.publicKey)));
190
+ return accounts;
191
+ }
192
+
193
+ public async getRegisteredAccount(address: AztecAddress): Promise<CompleteAddress | undefined> {
194
+ const result = await this.getRegisteredAccounts();
195
+ const account = result.find(r => r.address.equals(address));
196
+ return Promise.resolve(account);
197
+ }
198
+
199
+ public async registerRecipient(recipient: CompleteAddress): Promise<void> {
200
+ const wasAdded = await this.db.addCompleteAddress(recipient);
201
+ if (wasAdded) {
202
+ this.log.info(`Added recipient:\n ${recipient.toReadableString()}`);
203
+ } else {
204
+ this.log.info(`Recipient:\n "${recipient.toReadableString()}"\n already registered.`);
205
+ }
206
+ }
207
+
208
+ public async getRecipients(): Promise<CompleteAddress[]> {
209
+ // Get complete addresses of both the recipients and the accounts
210
+ const addresses = await this.db.getCompleteAddresses();
211
+ // Filter out the addresses corresponding to accounts
212
+ const accountPubKeys = await this.keyStore.getAccounts();
213
+ const recipients = addresses.filter(address => !accountPubKeys.find(pubKey => pubKey.equals(address.publicKey)));
214
+ return recipients;
215
+ }
216
+
217
+ public async getRecipient(address: AztecAddress): Promise<CompleteAddress | undefined> {
218
+ const result = await this.getRecipients();
219
+ const recipient = result.find(r => r.address.equals(address));
220
+ return Promise.resolve(recipient);
221
+ }
222
+
223
+ public async addContracts(contracts: DeployedContract[]) {
224
+ const contractDaos = contracts.map(c => new ContractDao(c.artifact, c.instance));
225
+ await Promise.all(contractDaos.map(c => this.db.addContract(c)));
226
+ await this.addArtifactsAndInstancesFromDeployedContracts(contracts);
227
+ for (const contract of contractDaos) {
228
+ const instance = contract.instance;
229
+ const contractAztecAddress = instance.address;
230
+ const hasPortal = instance.portalContractAddress && !instance.portalContractAddress.isZero();
231
+ const portalInfo = hasPortal ? ` with portal ${instance.portalContractAddress.toChecksumString()}` : '';
232
+ this.log.info(`Added contract ${contract.name} at ${contractAztecAddress}${portalInfo}`);
233
+ await this.synchronizer.reprocessDeferredNotesForContract(contractAztecAddress);
234
+ }
235
+ }
236
+
237
+ private async addArtifactsAndInstancesFromDeployedContracts(contracts: DeployedContract[]) {
238
+ for (const contract of contracts) {
239
+ const artifact = contract.artifact;
240
+ const artifactHash = computeArtifactHash(artifact);
241
+ const contractClassId = computeContractClassId(getContractClassFromArtifact({ ...artifact, artifactHash }));
242
+ await this.db.addContractArtifact(contractClassId, artifact);
243
+ await this.db.addContractInstance(contract.instance);
244
+ }
245
+ }
246
+
247
+ public async getContracts(): Promise<AztecAddress[]> {
248
+ return (await this.db.getContracts()).map(c => c.instance.address);
249
+ }
250
+
251
+ public async getPublicStorageAt(contract: AztecAddress, slot: Fr) {
252
+ if ((await this.getContractData(contract)) === undefined) {
253
+ throw new Error(`Contract ${contract.toString()} is not deployed`);
254
+ }
255
+ return await this.node.getPublicStorageAt(contract, slot);
256
+ }
257
+
258
+ public async getNotes(filter: NoteFilter): Promise<ExtendedNote[]> {
259
+ const noteDaos = await this.db.getNotes(filter);
260
+
261
+ // TODO(benesjan): Refactor --> This type conversion is ugly but I decided to keep it this way for now because
262
+ // key derivation will affect all this
263
+ const extendedNotes = noteDaos.map(async dao => {
264
+ let owner = filter.owner;
265
+ if (owner === undefined) {
266
+ const completeAddresses = (await this.db.getCompleteAddresses()).find(address =>
267
+ address.publicKey.equals(dao.publicKey),
268
+ );
269
+ if (completeAddresses === undefined) {
270
+ throw new Error(`Cannot find complete address for public key ${dao.publicKey.toString()}`);
271
+ }
272
+ owner = completeAddresses.address;
273
+ }
274
+ return new ExtendedNote(dao.note, owner, dao.contractAddress, dao.storageSlot, dao.noteTypeId, dao.txHash);
275
+ });
276
+ return Promise.all(extendedNotes);
277
+ }
278
+
279
+ public async addNote(note: ExtendedNote) {
280
+ const { publicKey } = (await this.db.getCompleteAddress(note.owner)) ?? {};
281
+ if (!publicKey) {
282
+ throw new Error('Unknown account.');
283
+ }
284
+
285
+ const nonces = await this.getNoteNonces(note);
286
+ if (nonces.length === 0) {
287
+ throw new Error(`Cannot find the note in tx: ${note.txHash}.`);
288
+ }
289
+
290
+ for (const nonce of nonces) {
291
+ const { innerNoteHash, siloedNoteHash, uniqueSiloedNoteHash, innerNullifier } =
292
+ await this.simulator.computeNoteHashAndNullifier(
293
+ note.contractAddress,
294
+ nonce,
295
+ note.storageSlot,
296
+ note.noteTypeId,
297
+ note.note,
298
+ );
299
+
300
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386)
301
+ // This can always be `uniqueSiloedNoteHash` once notes added from public also include nonces.
302
+ const noteHashToLookUp = nonce.isZero() ? siloedNoteHash : uniqueSiloedNoteHash;
303
+ const index = await this.node.findLeafIndex('latest', MerkleTreeId.NOTE_HASH_TREE, noteHashToLookUp);
304
+ if (index === undefined) {
305
+ throw new Error('Note does not exist.');
306
+ }
307
+
308
+ const siloedNullifier = siloNullifier(note.contractAddress, innerNullifier!);
309
+ const nullifierIndex = await this.node.findLeafIndex('latest', MerkleTreeId.NULLIFIER_TREE, siloedNullifier);
310
+ if (nullifierIndex !== undefined) {
311
+ throw new Error('The note has been destroyed.');
312
+ }
313
+
314
+ await this.db.addNote(
315
+ new NoteDao(
316
+ note.note,
317
+ note.contractAddress,
318
+ note.storageSlot,
319
+ note.noteTypeId,
320
+ note.txHash,
321
+ nonce,
322
+ innerNoteHash,
323
+ siloedNullifier,
324
+ index,
325
+ publicKey,
326
+ ),
327
+ );
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Finds the nonce(s) for a given note.
333
+ * @param note - The note to find the nonces for.
334
+ * @returns The nonces of the note.
335
+ * @remarks More than a single nonce may be returned since there might be more than one nonce for a given note.
336
+ */
337
+ private async getNoteNonces(note: ExtendedNote): Promise<Fr[]> {
338
+ const tx = await this.node.getTxEffect(note.txHash);
339
+ if (!tx) {
340
+ throw new Error(`Unknown tx: ${note.txHash}`);
341
+ }
342
+
343
+ const nonces: Fr[] = [];
344
+ const firstNullifier = tx.nullifiers[0];
345
+ const hashes = tx.noteHashes;
346
+ for (let i = 0; i < hashes.length; ++i) {
347
+ const hash = hashes[i];
348
+ if (hash.equals(Fr.ZERO)) {
349
+ break;
350
+ }
351
+
352
+ const nonce = computeCommitmentNonce(firstNullifier, i);
353
+ const { siloedNoteHash, uniqueSiloedNoteHash } = await this.simulator.computeNoteHashAndNullifier(
354
+ note.contractAddress,
355
+ nonce,
356
+ note.storageSlot,
357
+ note.noteTypeId,
358
+ note.note,
359
+ );
360
+ // TODO(https://github.com/AztecProtocol/aztec-packages/issues/1386)
361
+ // Remove this once notes added from public also include nonces.
362
+ if (hash.equals(siloedNoteHash)) {
363
+ nonces.push(Fr.ZERO);
364
+ break;
365
+ }
366
+ if (hash.equals(uniqueSiloedNoteHash)) {
367
+ nonces.push(nonce);
368
+ }
369
+ }
370
+
371
+ return nonces;
372
+ }
373
+
374
+ public async getBlock(blockNumber: number): Promise<L2Block | undefined> {
375
+ // If a negative block number is provided the current block number is fetched.
376
+ if (blockNumber < 0) {
377
+ blockNumber = await this.node.getBlockNumber();
378
+ }
379
+ return await this.node.getBlock(blockNumber);
380
+ }
381
+
382
+ public async simulateTx(txRequest: TxExecutionRequest, simulatePublic: boolean) {
383
+ if (!txRequest.functionData.isPrivate) {
384
+ throw new Error(`Public entrypoints are not allowed`);
385
+ }
386
+ if (txRequest.functionData.isInternal === undefined) {
387
+ throw new Error(`Unspecified internal are not allowed`);
388
+ }
389
+
390
+ // all simulations must be serialized w.r.t. the synchronizer
391
+ return await this.jobQueue.put(async () => {
392
+ // We get the contract address from origin, since contract deployments are signalled as origin from their own address
393
+ // TODO: Is this ok? Should it be changed to be from ZERO?
394
+ const deployedContractAddress = txRequest.txContext.isContractDeploymentTx ? txRequest.origin : undefined;
395
+ const newContract = deployedContractAddress ? await this.db.getContract(deployedContractAddress) : undefined;
396
+
397
+ const timer = new Timer();
398
+ const tx = await this.#simulateAndProve(txRequest, newContract);
399
+ this.log(`Processed private part of ${tx.getTxHash()}`, {
400
+ eventName: 'tx-pxe-processing',
401
+ duration: timer.ms(),
402
+ ...tx.getStats(),
403
+ } satisfies TxPXEProcessingStats);
404
+ if (simulatePublic) {
405
+ await this.#simulatePublicCalls(tx);
406
+ }
407
+ this.log.info(`Executed local simulation for ${tx.getTxHash()}`);
408
+
409
+ return tx;
410
+ });
411
+ }
412
+
413
+ public async sendTx(tx: Tx): Promise<TxHash> {
414
+ const txHash = tx.getTxHash();
415
+ if (await this.node.getTxEffect(txHash)) {
416
+ throw new Error(`A settled tx with equal hash ${txHash.toString()} exists.`);
417
+ }
418
+ this.log.info(`Sending transaction ${txHash}`);
419
+ await this.node.sendTx(tx);
420
+ return txHash;
421
+ }
422
+
423
+ public async viewTx(
424
+ functionName: string,
425
+ args: any[],
426
+ to: AztecAddress,
427
+ _from?: AztecAddress,
428
+ ): Promise<DecodedReturn> {
429
+ // all simulations must be serialized w.r.t. the synchronizer
430
+ return await this.jobQueue.put(async () => {
431
+ // TODO - Should check if `from` has the permission to call the view function.
432
+ const functionCall = await this.#getFunctionCall(functionName, args, to);
433
+ const executionResult = await this.#simulateUnconstrained(functionCall);
434
+
435
+ // TODO - Return typed result based on the function artifact.
436
+ return executionResult;
437
+ });
438
+ }
439
+
440
+ public getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
441
+ return this.node.getTxReceipt(txHash);
442
+ }
443
+
444
+ public getTxEffect(txHash: TxHash): Promise<TxEffect | undefined> {
445
+ return this.node.getTxEffect(txHash);
446
+ }
447
+
448
+ async getBlockNumber(): Promise<number> {
449
+ return await this.node.getBlockNumber();
450
+ }
451
+
452
+ public async getExtendedContractData(contractAddress: AztecAddress): Promise<ExtendedContractData | undefined> {
453
+ return await this.node.getExtendedContractData(contractAddress);
454
+ }
455
+
456
+ public async getContractData(contractAddress: AztecAddress): Promise<ContractData | undefined> {
457
+ return await this.node.getContractData(contractAddress);
458
+ }
459
+
460
+ /**
461
+ * Gets unencrypted logs based on the provided filter.
462
+ * @param filter - The filter to apply to the logs.
463
+ * @returns The requested logs.
464
+ */
465
+ public getUnencryptedLogs(filter: LogFilter): Promise<GetUnencryptedLogsResponse> {
466
+ return this.node.getUnencryptedLogs(filter);
467
+ }
468
+
469
+ async #getFunctionCall(functionName: string, args: any[], to: AztecAddress): Promise<FunctionCall> {
470
+ const contract = await this.db.getContract(to);
471
+ if (!contract) {
472
+ throw new Error(
473
+ `Unknown contract ${to}: add it to PXE Service by calling server.addContracts(...).\nSee docs for context: https://docs.aztec.network/developers/debugging/aztecnr-errors#unknown-contract-0x0-add-it-to-pxe-by-calling-serveraddcontracts`,
474
+ );
475
+ }
476
+
477
+ const functionDao = contract.functions.find(f => f.name === functionName);
478
+ if (!functionDao) {
479
+ throw new Error(`Unknown function ${functionName} in contract ${contract.name}.`);
480
+ }
481
+
482
+ return {
483
+ args: encodeArguments(functionDao, args),
484
+ functionData: FunctionData.fromAbi(functionDao),
485
+ to,
486
+ };
487
+ }
488
+
489
+ public async getNodeInfo(): Promise<NodeInfo> {
490
+ const [version, chainId, contractAddresses] = await Promise.all([
491
+ this.node.getVersion(),
492
+ this.node.getChainId(),
493
+ this.node.getL1ContractAddresses(),
494
+ ]);
495
+
496
+ const nodeInfo: NodeInfo = {
497
+ nodeVersion: this.nodeVersion,
498
+ chainId,
499
+ protocolVersion: version,
500
+ l1ContractAddresses: contractAddresses,
501
+ };
502
+ return nodeInfo;
503
+ }
504
+
505
+ /**
506
+ * Retrieves the simulation parameters required to run an ACIR simulation.
507
+ * This includes the contract address, function artifact, portal contract address, and historical tree roots.
508
+ *
509
+ * @param execRequest - The transaction request object containing details of the contract call.
510
+ * @returns An object containing the contract address, function artifact, portal contract address, and historical tree roots.
511
+ */
512
+ async #getSimulationParameters(execRequest: FunctionCall | TxExecutionRequest) {
513
+ const contractAddress = (execRequest as FunctionCall).to ?? (execRequest as TxExecutionRequest).origin;
514
+ const functionArtifact = await this.contractDataOracle.getFunctionArtifact(
515
+ contractAddress,
516
+ execRequest.functionData.selector,
517
+ );
518
+ const debug = await this.contractDataOracle.getFunctionDebugMetadata(
519
+ contractAddress,
520
+ execRequest.functionData.selector,
521
+ );
522
+ const portalContract = await this.contractDataOracle.getPortalContractAddress(contractAddress);
523
+
524
+ return {
525
+ contractAddress,
526
+ functionArtifact: {
527
+ ...functionArtifact,
528
+ debug,
529
+ },
530
+ portalContract,
531
+ };
532
+ }
533
+
534
+ async #simulate(txRequest: TxExecutionRequest): Promise<ExecutionResult> {
535
+ // TODO - Pause syncing while simulating.
536
+
537
+ const { contractAddress, functionArtifact, portalContract } = await this.#getSimulationParameters(txRequest);
538
+
539
+ this.log('Executing simulator...');
540
+ try {
541
+ const result = await this.simulator.run(txRequest, functionArtifact, contractAddress, portalContract);
542
+ this.log('Simulation completed!');
543
+ return result;
544
+ } catch (err) {
545
+ if (err instanceof SimulationError) {
546
+ await this.#enrichSimulationError(err);
547
+ }
548
+ throw err;
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Simulate an unconstrained transaction on the given contract, without considering constraints set by ACIR.
554
+ * The simulation parameters are fetched using ContractDataOracle and executed using AcirSimulator.
555
+ * Returns the simulation result containing the outputs of the unconstrained function.
556
+ *
557
+ * @param execRequest - The transaction request object containing the target contract and function data.
558
+ * @returns The simulation result containing the outputs of the unconstrained function.
559
+ */
560
+ async #simulateUnconstrained(execRequest: FunctionCall) {
561
+ const { contractAddress, functionArtifact } = await this.#getSimulationParameters(execRequest);
562
+
563
+ this.log('Executing unconstrained simulator...');
564
+ try {
565
+ const result = await this.simulator.runUnconstrained(execRequest, functionArtifact, contractAddress);
566
+ this.log('Unconstrained simulation completed!');
567
+
568
+ return result;
569
+ } catch (err) {
570
+ if (err instanceof SimulationError) {
571
+ await this.#enrichSimulationError(err);
572
+ }
573
+ throw err;
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Simulate the public part of a transaction.
579
+ * This allows to catch public execution errors before submitting the transaction.
580
+ * It can also be used for estimating gas in the future.
581
+ * @param tx - The transaction to be simulated.
582
+ */
583
+ async #simulatePublicCalls(tx: Tx) {
584
+ try {
585
+ await this.node.simulatePublicCalls(tx);
586
+ } catch (err) {
587
+ // Try to fill in the noir call stack since the PXE may have access to the debug metadata
588
+ if (err instanceof SimulationError) {
589
+ const callStack = err.getCallStack();
590
+ const originalFailingFunction = callStack[callStack.length - 1];
591
+ const debugInfo = await this.contractDataOracle.getFunctionDebugMetadata(
592
+ originalFailingFunction.contractAddress,
593
+ originalFailingFunction.functionSelector,
594
+ );
595
+ const noirCallStack = err.getNoirCallStack();
596
+ if (debugInfo && isNoirCallStackUnresolved(noirCallStack)) {
597
+ const parsedCallStack = resolveOpcodeLocations(noirCallStack, debugInfo);
598
+ err.setNoirCallStack(parsedCallStack);
599
+ }
600
+ await this.#enrichSimulationError(err);
601
+ }
602
+
603
+ throw err;
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Simulate a transaction, generate a kernel proof, and create a private transaction object.
609
+ * The function takes in a transaction request and an ECDSA signature. It simulates the transaction,
610
+ * then generates a kernel proof using the simulation result. Finally, it creates a private
611
+ * transaction object with the generated proof and public inputs. If a new contract address is provided,
612
+ * the function will also include the new contract's public functions in the transaction object.
613
+ *
614
+ * @param txExecutionRequest - The transaction request to be simulated and proved.
615
+ * @param signature - The ECDSA signature for the transaction request.
616
+ * @param newContract - Optional. The address of a new contract to be included in the transaction object.
617
+ * @returns A private transaction object containing the proof, public inputs, and encrypted logs.
618
+ */
619
+ async #simulateAndProve(txExecutionRequest: TxExecutionRequest, newContract: ContractDao | undefined) {
620
+ // TODO - Pause syncing while simulating.
621
+
622
+ // Get values that allow us to reconstruct the block hash
623
+ const executionResult = await this.#simulate(txExecutionRequest);
624
+
625
+ const kernelOracle = new KernelOracle(this.contractDataOracle, this.keyStore, this.node);
626
+ const kernelProver = new KernelProver(kernelOracle);
627
+ this.log(`Executing kernel prover...`);
628
+ const { proof, publicInputs } = await kernelProver.prove(txExecutionRequest.toTxRequest(), executionResult);
629
+ this.log(
630
+ `Needs setup: ${publicInputs.needsSetup}, needs app logic: ${publicInputs.needsAppLogic}, needs teardown: ${publicInputs.needsTeardown}`,
631
+ );
632
+
633
+ const encryptedLogs = new TxL2Logs(collectEncryptedLogs(executionResult));
634
+ const unencryptedLogs = new TxL2Logs(collectUnencryptedLogs(executionResult));
635
+ const enqueuedPublicFunctions = collectEnqueuedPublicFunctionCalls(executionResult);
636
+
637
+ const extendedContractData = newContract
638
+ ? new ExtendedContractData(
639
+ new ContractData(newContract.instance.address, newContract.instance.portalContractAddress),
640
+ getNewContractPublicFunctions(newContract),
641
+ getContractClassFromArtifact(newContract).id,
642
+ computeSaltedInitializationHash(newContract.instance),
643
+ newContract.instance.publicKeysHash,
644
+ )
645
+ : ExtendedContractData.empty();
646
+
647
+ // HACK(#1639): Manually patches the ordering of the public call stack
648
+ // TODO(#757): Enforce proper ordering of enqueued public calls
649
+ await this.patchPublicCallStackOrdering(publicInputs, enqueuedPublicFunctions);
650
+
651
+ return new Tx(publicInputs, proof, encryptedLogs, unencryptedLogs, enqueuedPublicFunctions, [extendedContractData]);
652
+ }
653
+
654
+ /**
655
+ * Adds contract and function names to a simulation error.
656
+ * @param err - The error to enrich.
657
+ */
658
+ async #enrichSimulationError(err: SimulationError) {
659
+ // Maps contract addresses to the set of functions selectors that were in error.
660
+ // Using strings because map and set don't use .equals()
661
+ const mentionedFunctions: Map<string, Set<string>> = new Map();
662
+
663
+ err.getCallStack().forEach(({ contractAddress, functionSelector }) => {
664
+ if (!mentionedFunctions.has(contractAddress.toString())) {
665
+ mentionedFunctions.set(contractAddress.toString(), new Set());
666
+ }
667
+ mentionedFunctions.get(contractAddress.toString())!.add(functionSelector.toString());
668
+ });
669
+
670
+ await Promise.all(
671
+ [...mentionedFunctions.entries()].map(async ([contractAddress, selectors]) => {
672
+ const parsedContractAddress = AztecAddress.fromString(contractAddress);
673
+ const contract = await this.db.getContract(parsedContractAddress);
674
+ if (contract) {
675
+ err.enrichWithContractName(parsedContractAddress, contract.name);
676
+ selectors.forEach(selector => {
677
+ const functionArtifact = contract.functions.find(f => f.selector.toString() === selector);
678
+ if (functionArtifact) {
679
+ err.enrichWithFunctionName(parsedContractAddress, functionArtifact.selector, functionArtifact.name);
680
+ }
681
+ });
682
+ }
683
+ }),
684
+ );
685
+ }
686
+
687
+ // HACK(#1639): this is a hack to fix ordering of public calls enqueued in the call stack. Since the private kernel
688
+ // cannot keep track of side effects that happen after or before a nested call, we override the public call stack
689
+ // it emits with whatever we got from the simulator collected enqueued calls. As a sanity check, we at least verify
690
+ // that the elements are the same, so we are only tweaking their ordering.
691
+ // See yarn-project/end-to-end/src/e2e_ordering.test.ts
692
+ // See https://github.com/AztecProtocol/aztec-packages/issues/1615
693
+ // TODO(#757): Enforce proper ordering of enqueued public calls
694
+ private async patchPublicCallStackOrdering(
695
+ publicInputs: PrivateKernelTailCircuitPublicInputs,
696
+ enqueuedPublicCalls: PublicCallRequest[],
697
+ ) {
698
+ const enqueuedPublicCallStackItems = await Promise.all(enqueuedPublicCalls.map(c => c.toCallRequest()));
699
+
700
+ // Validate all items in enqueued public calls are in the kernel emitted stack
701
+ const enqueuedRevertiblePublicCallStackItems = enqueuedPublicCallStackItems.filter(enqueued =>
702
+ publicInputs.end.publicCallStack.find(item => item.equals(enqueued)),
703
+ );
704
+
705
+ const revertibleStackSize = arrayNonEmptyLength(publicInputs.end.publicCallStack, item => item.isEmpty());
706
+
707
+ if (enqueuedRevertiblePublicCallStackItems.length !== revertibleStackSize) {
708
+ throw new Error(
709
+ `Enqueued revertible public function calls and revertible public call stack do not match.\nEnqueued calls: ${enqueuedRevertiblePublicCallStackItems
710
+ .map(h => h.hash.toString())
711
+ .join(', ')}\nPublic call stack: ${publicInputs.end.publicCallStack.map(i => i.toString()).join(', ')}`,
712
+ );
713
+ }
714
+
715
+ // Override kernel output
716
+ publicInputs.end.publicCallStack = padArrayEnd(
717
+ enqueuedRevertiblePublicCallStackItems,
718
+ CallRequest.empty(),
719
+ MAX_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
720
+ );
721
+
722
+ // Do the same for non-revertible
723
+
724
+ const enqueuedNonRevertiblePublicCallStackItems = enqueuedPublicCallStackItems.filter(enqueued =>
725
+ publicInputs.endNonRevertibleData.publicCallStack.find(item => item.equals(enqueued)),
726
+ );
727
+
728
+ const nonRevertibleStackSize = arrayNonEmptyLength(publicInputs.endNonRevertibleData.publicCallStack, item =>
729
+ item.isEmpty(),
730
+ );
731
+
732
+ if (enqueuedNonRevertiblePublicCallStackItems.length !== nonRevertibleStackSize) {
733
+ throw new Error(
734
+ `Enqueued non-revertible public function calls and non-revertible public call stack do not match.\nEnqueued calls: ${enqueuedNonRevertiblePublicCallStackItems
735
+ .map(h => h.hash.toString())
736
+ .join(', ')}\nPublic call stack: ${publicInputs.endNonRevertibleData.publicCallStack
737
+ .map(i => i.toString())
738
+ .join(', ')}`,
739
+ );
740
+ }
741
+
742
+ // Override kernel output
743
+ publicInputs.endNonRevertibleData.publicCallStack = padArrayEnd(
744
+ enqueuedNonRevertiblePublicCallStackItems,
745
+ CallRequest.empty(),
746
+ MAX_NON_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX,
747
+ );
748
+ }
749
+
750
+ public async isGlobalStateSynchronized() {
751
+ return await this.synchronizer.isGlobalStateSynchronized();
752
+ }
753
+
754
+ public async isAccountStateSynchronized(account: AztecAddress) {
755
+ return await this.synchronizer.isAccountStateSynchronized(account);
756
+ }
757
+
758
+ public getSyncStatus() {
759
+ return Promise.resolve(this.synchronizer.getSyncStatus());
760
+ }
761
+
762
+ public getKeyStore() {
763
+ return this.keyStore;
764
+ }
765
+
766
+ public async isContractClassPubliclyRegistered(id: Fr): Promise<boolean> {
767
+ return !!(await this.node.getContractClass(id));
768
+ }
769
+ }