@aztec/txe 0.0.1-commit.d3ec352c → 0.0.1-commit.d58ff9d0

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 (175) hide show
  1. package/dest/AuthRegistry-QVGDDH5X.js +3 -0
  2. package/dest/AuthRegistry-QVGDDH5X.js.map +7 -0
  3. package/dest/ContractClassRegistry-VWR3F42P.js +3 -0
  4. package/dest/ContractClassRegistry-VWR3F42P.js.map +7 -0
  5. package/dest/ContractInstanceRegistry-FHIWE33X.js +3 -0
  6. package/dest/ContractInstanceRegistry-FHIWE33X.js.map +7 -0
  7. package/dest/FeeJuice-SAOWVGG7.js +3 -0
  8. package/dest/FeeJuice-SAOWVGG7.js.map +7 -0
  9. package/dest/HandshakeRegistry-GY5CPEPS.js +3 -0
  10. package/dest/HandshakeRegistry-GY5CPEPS.js.map +7 -0
  11. package/dest/MultiCallEntrypoint-QG5H33ZZ.js +3 -0
  12. package/dest/MultiCallEntrypoint-QG5H33ZZ.js.map +7 -0
  13. package/dest/SchnorrAccount-NME3E45O.js +3 -0
  14. package/dest/SchnorrAccount-NME3E45O.js.map +7 -0
  15. package/dest/SchnorrInitializerlessAccount-WTGXU6SW.js +3 -0
  16. package/dest/SchnorrInitializerlessAccount-WTGXU6SW.js.map +7 -0
  17. package/dest/bin/check_txe_oracle_version.d.ts +2 -0
  18. package/dest/bin/check_txe_oracle_version.d.ts.map +1 -0
  19. package/dest/bin/check_txe_oracle_version.js +53 -0
  20. package/dest/bin/index.js +3 -30
  21. package/dest/bin/index.js.map +7 -0
  22. package/dest/bin/oracle_test_server.d.ts +3 -0
  23. package/dest/bin/oracle_test_server.d.ts.map +1 -0
  24. package/dest/bin/oracle_test_server.js +41 -0
  25. package/dest/chunk-32UHX7YY.js +3 -0
  26. package/dest/chunk-32UHX7YY.js.map +7 -0
  27. package/dest/chunk-GSUKUCKV.js +260 -0
  28. package/dest/chunk-GSUKUCKV.js.map +7 -0
  29. package/dest/chunk-UP4KKUVA.js +304 -0
  30. package/dest/chunk-UP4KKUVA.js.map +7 -0
  31. package/dest/constants.d.ts +7 -0
  32. package/dest/constants.d.ts.map +1 -0
  33. package/dest/constants.js +10 -0
  34. package/dest/dispatcher_pool.d.ts +67 -0
  35. package/dest/dispatcher_pool.d.ts.map +1 -0
  36. package/dest/dispatcher_pool.js +286 -0
  37. package/dest/index.d.ts +51 -7
  38. package/dest/index.d.ts.map +1 -1
  39. package/dest/index.js +75 -158
  40. package/dest/metafile.json +38759 -0
  41. package/dest/msgpackr_fr_extension.d.ts +2 -0
  42. package/dest/msgpackr_fr_extension.d.ts.map +1 -0
  43. package/dest/msgpackr_fr_extension.js +21 -0
  44. package/dest/oracle/interfaces.d.ts +71 -30
  45. package/dest/oracle/interfaces.d.ts.map +1 -1
  46. package/dest/oracle/tagging_secret_strategy.d.ts +12 -0
  47. package/dest/oracle/tagging_secret_strategy.d.ts.map +1 -0
  48. package/dest/oracle/tagging_secret_strategy.js +12 -0
  49. package/dest/oracle/test-resolver/default_fixtures.d.ts +16 -0
  50. package/dest/oracle/test-resolver/default_fixtures.d.ts.map +1 -0
  51. package/dest/oracle/test-resolver/default_fixtures.js +200 -0
  52. package/dest/oracle/test-resolver/index.d.ts +9 -0
  53. package/dest/oracle/test-resolver/index.d.ts.map +1 -0
  54. package/dest/oracle/test-resolver/index.js +32 -0
  55. package/dest/oracle/test-resolver/resolver.d.ts +48 -0
  56. package/dest/oracle/test-resolver/resolver.d.ts.map +1 -0
  57. package/dest/oracle/test-resolver/resolver.js +181 -0
  58. package/dest/oracle/txe_oracle_public_context.d.ts +40 -16
  59. package/dest/oracle/txe_oracle_public_context.d.ts.map +1 -1
  60. package/dest/oracle/txe_oracle_public_context.js +69 -23
  61. package/dest/oracle/txe_oracle_registry.d.ts +172 -0
  62. package/dest/oracle/txe_oracle_registry.d.ts.map +1 -0
  63. package/dest/oracle/txe_oracle_registry.js +767 -0
  64. package/dest/oracle/txe_oracle_top_level_context.d.ts +79 -30
  65. package/dest/oracle/txe_oracle_top_level_context.d.ts.map +1 -1
  66. package/dest/oracle/txe_oracle_top_level_context.js +653 -285
  67. package/dest/oracle/txe_oracle_version.d.ts +17 -0
  68. package/dest/oracle/txe_oracle_version.d.ts.map +1 -0
  69. package/dest/oracle/txe_oracle_version.js +14 -0
  70. package/dest/oracle/txe_private_execution_oracle.d.ts +17 -0
  71. package/dest/oracle/txe_private_execution_oracle.d.ts.map +1 -0
  72. package/dest/oracle/txe_private_execution_oracle.js +15 -0
  73. package/dest/rpc_server.d.ts +14 -0
  74. package/dest/rpc_server.d.ts.map +1 -0
  75. package/dest/rpc_server.js +78 -0
  76. package/dest/rpc_translator.d.ts +115 -216
  77. package/dest/rpc_translator.d.ts.map +1 -1
  78. package/dest/rpc_translator.js +901 -560
  79. package/dest/server.bundle.js +3 -0
  80. package/dest/server.bundle.js.map +7 -0
  81. package/dest/state_machine/archiver.d.ts +23 -58
  82. package/dest/state_machine/archiver.d.ts.map +1 -1
  83. package/dest/state_machine/archiver.js +76 -112
  84. package/dest/state_machine/dummy_p2p_client.d.ts +31 -17
  85. package/dest/state_machine/dummy_p2p_client.d.ts.map +1 -1
  86. package/dest/state_machine/dummy_p2p_client.js +63 -25
  87. package/dest/state_machine/global_variable_builder.d.ts +10 -5
  88. package/dest/state_machine/global_variable_builder.d.ts.map +1 -1
  89. package/dest/state_machine/global_variable_builder.js +20 -5
  90. package/dest/state_machine/index.d.ts +14 -8
  91. package/dest/state_machine/index.d.ts.map +1 -1
  92. package/dest/state_machine/index.js +84 -27
  93. package/dest/state_machine/mock_epoch_cache.d.ts +24 -8
  94. package/dest/state_machine/mock_epoch_cache.d.ts.map +1 -1
  95. package/dest/state_machine/mock_epoch_cache.js +43 -9
  96. package/dest/state_machine/synchronizer.d.ts +14 -6
  97. package/dest/state_machine/synchronizer.d.ts.map +1 -1
  98. package/dest/state_machine/synchronizer.js +23 -10
  99. package/dest/txe_session.d.ts +102 -20
  100. package/dest/txe_session.d.ts.map +1 -1
  101. package/dest/txe_session.js +442 -75
  102. package/dest/utils/block_creation.d.ts +20 -6
  103. package/dest/utils/block_creation.d.ts.map +1 -1
  104. package/dest/utils/block_creation.js +40 -6
  105. package/dest/utils/encoding.d.ts +192 -0
  106. package/dest/utils/encoding.d.ts.map +1 -0
  107. package/dest/{util → utils}/encoding.js +8 -3
  108. package/dest/{util → utils}/expected_failure_error.d.ts +1 -1
  109. package/dest/utils/expected_failure_error.d.ts.map +1 -0
  110. package/dest/utils/tx_effect_creation.d.ts +2 -3
  111. package/dest/utils/tx_effect_creation.d.ts.map +1 -1
  112. package/dest/utils/tx_effect_creation.js +3 -7
  113. package/dest/utils/txe_account_store.d.ts +10 -0
  114. package/dest/utils/txe_account_store.d.ts.map +1 -0
  115. package/dest/{util/txe_account_data_provider.js → utils/txe_account_store.js} +1 -1
  116. package/dest/utils/txe_artifact_resolver.d.ts +37 -0
  117. package/dest/utils/txe_artifact_resolver.d.ts.map +1 -0
  118. package/dest/utils/txe_artifact_resolver.js +161 -0
  119. package/dest/utils/txe_public_contract_data_source.d.ts +20 -0
  120. package/dest/utils/txe_public_contract_data_source.d.ts.map +1 -0
  121. package/dest/utils/txe_public_contract_data_source.js +51 -0
  122. package/dest/worker.bundle.js +3 -0
  123. package/dest/worker.bundle.js.map +7 -0
  124. package/dest/worker.d.ts +2 -0
  125. package/dest/worker.d.ts.map +1 -0
  126. package/dest/worker.js +92 -0
  127. package/package.json +39 -22
  128. package/src/bin/check_txe_oracle_version.ts +67 -0
  129. package/src/bin/index.ts +11 -2
  130. package/src/bin/oracle_test_server.ts +51 -0
  131. package/src/constants.ts +13 -0
  132. package/src/dispatcher_pool.ts +317 -0
  133. package/src/index.ts +113 -201
  134. package/src/msgpackr_fr_extension.ts +23 -0
  135. package/src/oracle/interfaces.ts +77 -35
  136. package/src/oracle/tagging_secret_strategy.ts +25 -0
  137. package/src/oracle/test-resolver/default_fixtures.ts +271 -0
  138. package/src/oracle/test-resolver/index.ts +44 -0
  139. package/src/oracle/test-resolver/resolver.ts +245 -0
  140. package/src/oracle/txe_oracle_public_context.ts +82 -27
  141. package/src/oracle/txe_oracle_registry.ts +556 -0
  142. package/src/oracle/txe_oracle_top_level_context.ts +555 -189
  143. package/src/oracle/txe_oracle_version.ts +17 -0
  144. package/src/oracle/txe_private_execution_oracle.ts +30 -0
  145. package/src/rpc_server.ts +87 -0
  146. package/src/rpc_translator.ts +1115 -923
  147. package/src/state_machine/archiver.ts +81 -132
  148. package/src/state_machine/dummy_p2p_client.ts +96 -38
  149. package/src/state_machine/global_variable_builder.ts +28 -10
  150. package/src/state_machine/index.ts +118 -44
  151. package/src/state_machine/mock_epoch_cache.ts +53 -14
  152. package/src/state_machine/synchronizer.ts +32 -12
  153. package/src/txe_session.ts +727 -123
  154. package/src/utils/block_creation.ts +50 -16
  155. package/src/{util → utils}/encoding.ts +9 -3
  156. package/src/utils/tx_effect_creation.ts +3 -13
  157. package/src/{util/txe_account_data_provider.ts → utils/txe_account_store.ts} +1 -1
  158. package/src/utils/txe_artifact_resolver.ts +217 -0
  159. package/src/utils/txe_public_contract_data_source.ts +60 -0
  160. package/src/worker.ts +98 -0
  161. package/dest/util/encoding.d.ts +0 -121
  162. package/dest/util/encoding.d.ts.map +0 -1
  163. package/dest/util/expected_failure_error.d.ts.map +0 -1
  164. package/dest/util/txe_account_data_provider.d.ts +0 -10
  165. package/dest/util/txe_account_data_provider.d.ts.map +0 -1
  166. package/dest/util/txe_contract_data_provider.d.ts +0 -12
  167. package/dest/util/txe_contract_data_provider.d.ts.map +0 -1
  168. package/dest/util/txe_contract_data_provider.js +0 -22
  169. package/dest/util/txe_public_contract_data_source.d.ts +0 -21
  170. package/dest/util/txe_public_contract_data_source.d.ts.map +0 -1
  171. package/dest/util/txe_public_contract_data_source.js +0 -67
  172. package/src/util/txe_contract_data_provider.ts +0 -36
  173. package/src/util/txe_public_contract_data_source.ts +0 -86
  174. /package/dest/{util → utils}/expected_failure_error.js +0 -0
  175. /package/src/{util → utils}/expected_failure_error.ts +0 -0
@@ -0,0 +1,317 @@
1
+ import { getSchnorrAccountContractArtifact } from '@aztec/accounts/schnorr/lazy';
2
+ import { BackendType, Barretenberg, BarretenbergSync } from '@aztec/bb.js';
3
+ import type { Logger } from '@aztec/foundation/log';
4
+ import { openEphemeralStore } from '@aztec/kv-store/lmdb-v2';
5
+ import { LazyProtocolContractsProvider } from '@aztec/protocol-contracts/providers/lazy';
6
+ import { ContractStore } from '@aztec/pxe/client/lazy';
7
+ import { getStandardAuthRegistry } from '@aztec/standard-contracts/auth-registry/lazy';
8
+ import { getStandardHandshakeRegistry } from '@aztec/standard-contracts/handshake-registry/lazy';
9
+ import { getContractClassFromArtifact } from '@aztec/stdlib/contract';
10
+
11
+ import { existsSync } from 'node:fs';
12
+ import { cpus } from 'node:os';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { Worker } from 'node:worker_threads';
15
+
16
+ import { TXE_REQUIRED_PROTOCOL_CONTRACTS } from './index.js';
17
+ import type { TXEForeignCallInput } from './index.js';
18
+ import type { ForeignCallResult } from './utils/encoding.js';
19
+
20
+ void Barretenberg.initSingleton({ backend: BackendType.Wasm, skipSrsInit: true, threads: 1 });
21
+ void BarretenbergSync.initSingleton({ backend: BackendType.Wasm });
22
+
23
+ /**
24
+ * Opens a fresh LMDB in a tmp dir and writes the protocol contracts in
25
+ * {@link TXE_REQUIRED_PROTOCOL_CONTRACTS} plus the standard AuthRegistry, HandshakeRegistry, and the SchnorrAccount artifact,
26
+ * returning the directory path and the SchnorrAccount class id (hex). The store handle is intentionally kept
27
+ * alive: closing it would trigger the ephemeral-store cleanup hook and remove the tmp
28
+ * directory, so any worker that has not yet cloned would find it missing.
29
+ */
30
+ export async function buildSharedContractStore(): Promise<{ dataDir: string; schnorrClassId: string }> {
31
+ const kvStore = await openEphemeralStore('txe-shared-contracts', undefined, 2);
32
+ const dataDir = kvStore.dataDirectory;
33
+ const contractStore = new ContractStore(kvStore);
34
+ const provider = new LazyProtocolContractsProvider();
35
+ const [protocolContracts, standardContracts, schnorrArtifact] = await Promise.all([
36
+ Promise.all(TXE_REQUIRED_PROTOCOL_CONTRACTS.map(name => provider.getProtocolContractArtifact(name))),
37
+ Promise.all([getStandardAuthRegistry(), getStandardHandshakeRegistry()]),
38
+ getSchnorrAccountContractArtifact(),
39
+ ]);
40
+ const schnorrClass = await getContractClassFromArtifact(schnorrArtifact);
41
+ await Promise.all([
42
+ ...[...protocolContracts, ...standardContracts].flatMap(({ instance, artifact, contractClass }) => [
43
+ contractStore.addContractArtifact(artifact, contractClass),
44
+ contractStore.addContractInstance(instance),
45
+ ]),
46
+ contractStore.addContractArtifact(schnorrArtifact, schnorrClass),
47
+ ]);
48
+ return { dataDir, schnorrClassId: schnorrClass.id.toString() };
49
+ }
50
+
51
+ /**
52
+ * Resolves `worker.bundle.js` whether this code is running unbundled (next to dispatcher_pool.js
53
+ * inside `dest/`) or bundled into `dest/bin/index.js` (one directory deeper). `import.meta.url`
54
+ * refers to whichever module the calling code actually lives in; we try both relative locations
55
+ * and use whichever exists.
56
+ */
57
+ function resolveWorkerBundlePath(): URL {
58
+ const candidates = [new URL('./worker.bundle.js', import.meta.url), new URL('../worker.bundle.js', import.meta.url)];
59
+ return candidates.find(u => existsSync(fileURLToPath(u))) ?? candidates[0];
60
+ }
61
+
62
+ interface SerializedError {
63
+ message: string;
64
+ name?: string;
65
+ stack?: string;
66
+ }
67
+
68
+ type WorkerMessage =
69
+ | { type: 'result'; requestId: number; ok: true; value: ForeignCallResult }
70
+ | { type: 'result'; requestId: number; ok: false; error: SerializedError }
71
+ | {
72
+ type: 'memstat';
73
+ sessions: number;
74
+ rss: number;
75
+ heapTotal: number;
76
+ heapUsed: number;
77
+ external: number;
78
+ arrayBuffers: number;
79
+ };
80
+
81
+ interface WorkerSlot {
82
+ worker: Worker;
83
+ sessions: Set<number>;
84
+ inFlightRequestIds: Set<number>;
85
+ }
86
+
87
+ interface PendingRequest {
88
+ resolve: (value: ForeignCallResult) => void;
89
+ reject: (err: Error) => void;
90
+ workerIdx: number;
91
+ }
92
+
93
+ function deserializeError(payload: SerializedError | undefined): Error {
94
+ if (!payload) {
95
+ return new Error('Worker returned an error with no payload');
96
+ }
97
+ const err = new Error(payload.message);
98
+ if (payload.name) {
99
+ err.name = payload.name;
100
+ }
101
+ if (payload.stack) {
102
+ err.stack = payload.stack;
103
+ }
104
+ return err;
105
+ }
106
+
107
+ export interface TXEDispatcherPoolOptions {
108
+ /** Number of worker threads */
109
+ workers?: number;
110
+ }
111
+
112
+ /**
113
+ * Main-thread router that owns a pool of TXE worker threads. Each worker runs its own
114
+ * {@link TXEDispatcher} and handles foreign-call requests for the sessions assigned to it.
115
+ *
116
+ * Routing is sticky by `session_id`: new sessions go to a freshly spawned worker (up to
117
+ * `maxWorkers`) or, once the cap is reached, to the existing worker with the fewest sessions.
118
+ * Each session's state — TXESession, native world state, KV stores — stays single-threaded
119
+ * within its worker; different sessions run in parallel across workers.
120
+ *
121
+ * The main thread builds a shared protocol-contracts LMDB once and passes its path via
122
+ * `workerData`. Workers clone the data file on demand instead of re-registering the contracts.
123
+ */
124
+ export class TXEDispatcherPool {
125
+ private readonly workers: WorkerSlot[] = [];
126
+ private readonly sessionToWorker = new Map<number, number>();
127
+ private readonly pending = new Map<number, PendingRequest>();
128
+ private nextRequestId = 0;
129
+ private readonly maxWorkers: number;
130
+ private readonly readyPromise: Promise<void>;
131
+ private contractStoreSourceDir?: string;
132
+ private schnorrClassId?: string;
133
+ private workerPath?: URL;
134
+
135
+ constructor(
136
+ private readonly logger: Logger,
137
+ opts: TXEDispatcherPoolOptions = {},
138
+ ) {
139
+ // TXE still doesn't scale well beyond 16 workers due to contention
140
+ this.maxWorkers = Math.max(1, opts.workers ?? Math.min(cpus().length, 16));
141
+ this.readyPromise = this.init();
142
+ }
143
+
144
+ /** Resolves once the shared protocol-contracts store is built. Workers spawn lazily on demand. */
145
+ public ready(): Promise<void> {
146
+ return this.readyPromise;
147
+ }
148
+
149
+ private async init(): Promise<void> {
150
+ const t0 = Date.now();
151
+ const { dataDir, schnorrClassId } = await buildSharedContractStore();
152
+ this.contractStoreSourceDir = dataDir;
153
+ this.schnorrClassId = schnorrClassId;
154
+ this.workerPath = resolveWorkerBundlePath();
155
+ this.logger.debug(`TXE dispatcher pool ready (lazy spawn, cap=${this.maxWorkers})`, {
156
+ contractStoreSourceDir: dataDir,
157
+ schnorrClassId,
158
+ ms: Date.now() - t0,
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Spawns a fresh worker and returns its index in `this.workers`. Caller must have awaited
164
+ * `init()` so the shared contract store path is set. Messages posted before the worker has
165
+ * finished loading are queued by Node's worker_threads transport.
166
+ */
167
+ private spawnWorker(): number {
168
+ const workerIdx = this.workers.length;
169
+ const w = new Worker(this.workerPath!, {
170
+ workerData: { contractStoreSourceDir: this.contractStoreSourceDir, schnorrClassId: this.schnorrClassId },
171
+ });
172
+ const slot: WorkerSlot = {
173
+ worker: w,
174
+ sessions: new Set(),
175
+ inFlightRequestIds: new Set(),
176
+ };
177
+ this.workers.push(slot);
178
+ w.on('message', (msg: WorkerMessage) => this.handleMessage(workerIdx, msg));
179
+ w.on('error', err => this.handleWorkerError(workerIdx, err));
180
+ w.on('exit', code => {
181
+ if (code !== 0) {
182
+ this.handleWorkerError(workerIdx, new Error(`Worker ${workerIdx} exited with code ${code}`));
183
+ }
184
+ });
185
+ this.logger.debug(`Spawning TXE worker ${workerIdx} on demand`, {
186
+ cap: this.maxWorkers,
187
+ poolSize: this.workers.length,
188
+ });
189
+ return workerIdx;
190
+ }
191
+
192
+ /** Routes a session-dispose request to the worker that owns the session. Fire-and-forget. */
193
+ disposeSession(sessionId: number): void {
194
+ const workerIdx = this.sessionToWorker.get(sessionId);
195
+ if (workerIdx === undefined) {
196
+ throw new Error(`disposeSession: no worker mapped for session ${sessionId}`);
197
+ }
198
+ this.sessionToWorker.delete(sessionId);
199
+ const slot = this.workers[workerIdx];
200
+ if (!slot) {
201
+ throw new Error(`disposeSession: worker ${workerIdx} (session ${sessionId}) missing from pool`);
202
+ }
203
+ slot.sessions.delete(sessionId);
204
+ slot.worker.postMessage({ type: 'dispose-session', sessionId });
205
+ }
206
+
207
+ // eslint-disable-next-line camelcase
208
+ async resolve_foreign_call(callData: TXEForeignCallInput): Promise<ForeignCallResult> {
209
+ // Make sure the shared contract store + worker bundle path are in place before we spawn.
210
+ await this.readyPromise;
211
+ const sessionId = callData.session_id;
212
+ let workerIdx = this.sessionToWorker.get(sessionId);
213
+ if (workerIdx === undefined) {
214
+ workerIdx = this.pickOrSpawnWorker();
215
+ this.sessionToWorker.set(sessionId, workerIdx);
216
+ this.workers[workerIdx].sessions.add(sessionId);
217
+ this.logger.debug(`Routing new session ${sessionId} to worker ${workerIdx}`);
218
+ }
219
+
220
+ const slot = this.workers[workerIdx];
221
+ const requestId = this.nextRequestId++;
222
+ return new Promise<ForeignCallResult>((resolve, reject) => {
223
+ this.pending.set(requestId, { resolve, reject, workerIdx: workerIdx! });
224
+ slot.inFlightRequestIds.add(requestId);
225
+ slot.worker.postMessage({ type: 'foreign-call', requestId, callData });
226
+ });
227
+ }
228
+
229
+ /** Terminates all workers. Used by tests; production TXE exits the process directly. */
230
+ async shutdown(): Promise<void> {
231
+ const slots = this.workers.splice(0, this.workers.length);
232
+ await Promise.all(slots.map(s => s.worker.terminate()));
233
+ for (const [requestId, pending] of this.pending) {
234
+ pending.reject(new Error('TXE dispatcher pool was shut down'));
235
+ this.pending.delete(requestId);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Returns the index of the worker that should handle a new session. Spawns a fresh worker if
241
+ * the pool hasn't reached its cap; otherwise picks the existing worker with the fewest
242
+ * assigned sessions. Always returns a valid index into `this.workers`.
243
+ */
244
+ private pickOrSpawnWorker(): number {
245
+ if (this.workers.length < this.maxWorkers) {
246
+ return this.spawnWorker();
247
+ }
248
+ let minIdx = 0;
249
+ let minCount = this.workers[0].sessions.size;
250
+ for (let i = 1; i < this.workers.length; i++) {
251
+ const count = this.workers[i].sessions.size;
252
+ if (count < minCount) {
253
+ minIdx = i;
254
+ minCount = count;
255
+ }
256
+ }
257
+ return minIdx;
258
+ }
259
+
260
+ private handleMessage(workerIdx: number, msg: WorkerMessage): void {
261
+ switch (msg.type) {
262
+ case 'result':
263
+ this.handleResult(workerIdx, msg);
264
+ return;
265
+ case 'memstat':
266
+ this.logger.debug(`worker ${workerIdx} memstat`, {
267
+ worker: workerIdx,
268
+ sessions: msg.sessions,
269
+ rssMiB: Math.round(msg.rss / 1024 / 1024),
270
+ heapTotalMiB: Math.round(msg.heapTotal / 1024 / 1024),
271
+ heapUsedMiB: Math.round(msg.heapUsed / 1024 / 1024),
272
+ externalMiB: Math.round(msg.external / 1024 / 1024),
273
+ arrayBuffersMiB: Math.round(msg.arrayBuffers / 1024 / 1024),
274
+ });
275
+ return;
276
+ }
277
+ }
278
+
279
+ private handleResult(workerIdx: number, msg: WorkerMessage & { type: 'result' }): void {
280
+ const pending = this.pending.get(msg.requestId);
281
+ if (!pending) {
282
+ throw new Error(`handleResult: request ${msg.requestId} (worker ${workerIdx}) not in pending map`);
283
+ }
284
+ this.pending.delete(msg.requestId);
285
+ const slot = this.workers[workerIdx];
286
+ if (!slot) {
287
+ throw new Error(`handleResult: worker ${workerIdx} (request ${msg.requestId}) missing from pool`);
288
+ }
289
+ slot.inFlightRequestIds.delete(msg.requestId);
290
+ if (msg.ok) {
291
+ pending.resolve(msg.value);
292
+ } else {
293
+ pending.reject(deserializeError(msg.error));
294
+ }
295
+ }
296
+
297
+ private handleWorkerError(workerIdx: number, err: Error): void {
298
+ this.logger.error(`TXE worker ${workerIdx} crashed; sessions assigned to it will fail`, err);
299
+ const slot = this.workers[workerIdx];
300
+ if (!slot) {
301
+ throw new Error(`handleWorkerError: worker ${workerIdx} missing from pool (orig err: ${err.message})`);
302
+ }
303
+ for (const requestId of slot.inFlightRequestIds) {
304
+ const pending = this.pending.get(requestId);
305
+ if (!pending) {
306
+ throw new Error(`handleWorkerError: in-flight request ${requestId} (worker ${workerIdx}) not in pending map`);
307
+ }
308
+ pending.reject(err);
309
+ this.pending.delete(requestId);
310
+ }
311
+ slot.inFlightRequestIds.clear();
312
+ for (const sessionId of slot.sessions) {
313
+ this.sessionToWorker.delete(sessionId);
314
+ }
315
+ slot.sessions.clear();
316
+ }
317
+ }
package/src/index.ts CHANGED
@@ -1,51 +1,35 @@
1
- import { SchnorrAccountContractArtifact } from '@aztec/accounts/schnorr';
2
- import { type NoirCompiledContract, loadContractArtifact } from '@aztec/aztec.js/abi';
3
- import { AztecAddress } from '@aztec/aztec.js/addresses';
4
- import {
5
- type ContractInstanceWithAddress,
6
- getContractInstanceFromInstantiationParams,
7
- } from '@aztec/aztec.js/contracts';
8
1
  import { Fr } from '@aztec/aztec.js/fields';
9
- import { PublicKeys, deriveKeys } from '@aztec/aztec.js/keys';
10
- import { createSafeJsonRpcServer } from '@aztec/foundation/json-rpc/server';
11
2
  import type { Logger } from '@aztec/foundation/log';
12
- import { type ProtocolContract, protocolContractNames } from '@aztec/protocol-contracts';
13
- import { BundledProtocolContractsProvider } from '@aztec/protocol-contracts/providers/bundle';
14
- import { computeArtifactHash } from '@aztec/stdlib/contract';
15
- import type { ApiSchemaFor, ZodFor } from '@aztec/stdlib/schemas';
16
-
17
- import { createHash } from 'crypto';
18
- import { createReadStream } from 'fs';
19
- import { readFile, readdir } from 'fs/promises';
20
- import { join, parse } from 'path';
3
+ import { cloneEphemeralStoreFrom } from '@aztec/kv-store/lmdb-v2';
4
+ import type { ProtocolContractName } from '@aztec/protocol-contracts';
5
+ import { ContractStore } from '@aztec/pxe/client/lazy';
6
+ import type { ApiSchemaFor } from '@aztec/stdlib/schemas';
7
+ import { zodFor } from '@aztec/stdlib/schemas';
8
+
9
+ import { join } from 'path';
21
10
  import { z } from 'zod';
22
11
 
12
+ // Side-effect import: registers the msgpackr Fr extension for the bundled `Fr` class. Must
13
+ // be loaded before any `sendMessage` call. See msgpackr_fr_extension.ts for the why.
14
+ import './msgpackr_fr_extension.js';
23
15
  import { type TXEOracleFunctionName, TXESession } from './txe_session.js';
24
16
  import {
25
17
  type ForeignCallArgs,
26
18
  ForeignCallArgsSchema,
27
- type ForeignCallArray,
28
19
  type ForeignCallResult,
29
20
  ForeignCallResultSchema,
30
- type ForeignCallSingle,
31
- fromArray,
32
- fromSingle,
33
- toSingle,
34
- } from './util/encoding.js';
35
- import type { ContractArtifactWithHash } from './util/txe_contract_data_provider.js';
21
+ } from './utils/encoding.js';
22
+ import { TXEArtifactResolver } from './utils/txe_artifact_resolver.js';
36
23
 
37
- const sessions = new Map<number, TXESession>();
24
+ export const TXE_REQUIRED_PROTOCOL_CONTRACTS: ProtocolContractName[] = [
25
+ 'ContractClassRegistry',
26
+ 'ContractInstanceRegistry',
27
+ 'FeeJuice',
28
+ ];
38
29
 
39
- /*
40
- * TXE typically has to load the same contract artifacts over and over again for multiple tests,
41
- * so we cache them here to avoid both loading them from disk repeatedly and computing their artifact hashes
42
- */
43
- const TXEArtifactsCache = new Map<
44
- string,
45
- { artifact: ContractArtifactWithHash; instance: ContractInstanceWithAddress }
46
- >();
30
+ const sessions = new Map<number, TXESession>();
47
31
 
48
- type TXEForeignCallInput = {
32
+ export type TXEForeignCallInput = {
49
33
  session_id: number;
50
34
  function: TXEOracleFunctionName;
51
35
  root_path: string;
@@ -53,189 +37,117 @@ type TXEForeignCallInput = {
53
37
  inputs: ForeignCallArgs;
54
38
  };
55
39
 
56
- const TXEForeignCallInputSchema = z.object({
57
- // eslint-disable-next-line camelcase
58
- session_id: z.number().int().nonnegative(),
59
- function: z.string() as ZodFor<TXEOracleFunctionName>,
60
- // eslint-disable-next-line camelcase
61
- root_path: z.string(),
62
- // eslint-disable-next-line camelcase
63
- package_name: z.string(),
64
- inputs: ForeignCallArgsSchema,
65
- }) satisfies ZodFor<TXEForeignCallInput>;
66
-
67
- class TXEDispatcher {
68
- private protocolContracts!: ProtocolContract[];
69
-
70
- constructor(private logger: Logger) {}
71
-
72
- private fastHashFile(path: string) {
73
- return new Promise(resolve => {
74
- const fd = createReadStream(path);
75
- const hash = createHash('sha1');
76
- hash.setEncoding('hex');
77
-
78
- fd.on('end', function () {
79
- hash.end();
80
- resolve(hash.read());
81
- });
82
-
83
- fd.pipe(hash);
84
- });
85
- }
86
-
87
- async #processDeployInputs({ inputs, root_path: rootPath, package_name: packageName }: TXEForeignCallInput) {
88
- const [contractPath, initializer] = inputs.slice(0, 2).map(input =>
89
- fromArray(input as ForeignCallArray)
90
- .map(char => String.fromCharCode(char.toNumber()))
91
- .join(''),
92
- );
93
-
94
- const decodedArgs = fromArray(inputs[3] as ForeignCallArray);
95
- const secret = fromSingle(inputs[4] as ForeignCallSingle);
96
- const publicKeys = secret.equals(Fr.ZERO) ? PublicKeys.default() : (await deriveKeys(secret)).publicKeys;
97
- const publicKeysHash = await publicKeys.hash();
98
-
99
- let artifactPath = '';
100
- const { dir: contractDirectory, base: contractFilename } = parse(contractPath);
101
- if (contractDirectory) {
102
- if (contractDirectory.includes('@')) {
103
- // We're deploying a contract that belongs in a workspace
104
- // env.deploy("../path/to/workspace/root@packageName/contractName")
105
- const [workspace, pkg] = contractDirectory.split('@');
106
- const targetPath = join(rootPath, workspace, '/target');
107
- this.logger.debug(`Looking for compiled artifact in workspace ${targetPath}`);
108
- artifactPath = join(targetPath, `${pkg}-${contractFilename}.json`);
109
- } else {
110
- // We're deploying a standalone external contract
111
- // env.deploy("../path/to/contract/root/contractName")
112
- const targetPath = join(rootPath, contractDirectory, '/target');
113
- this.logger.debug(`Looking for compiled artifact in ${targetPath}`);
114
- [artifactPath] = (await readdir(targetPath)).filter(file => file.endsWith(`-${contractFilename}.json`));
115
- }
116
- } else {
117
- // We're deploying a local contract
118
- // env.deploy("contractName")
119
- artifactPath = join(rootPath, './target', `${packageName}-${contractFilename}.json`);
120
- }
121
-
122
- const fileHash = await this.fastHashFile(artifactPath);
123
-
124
- const cacheKey = `${contractDirectory ?? ''}-${contractFilename}-${initializer}-${decodedArgs
125
- .map(arg => arg.toString())
126
- .join('-')}-${publicKeysHash}-${fileHash}`;
127
-
128
- let instance;
129
- let artifact: ContractArtifactWithHash;
130
-
131
- if (TXEArtifactsCache.has(cacheKey)) {
132
- this.logger.debug(`Using cached artifact for ${cacheKey}`);
133
- ({ artifact, instance } = TXEArtifactsCache.get(cacheKey)!);
134
- } else {
135
- this.logger.debug(`Loading compiled artifact ${artifactPath}`);
136
- const artifactJSON = JSON.parse(await readFile(artifactPath, 'utf-8')) as NoirCompiledContract;
137
- const artifactWithoutHash = loadContractArtifact(artifactJSON);
138
- artifact = {
139
- ...artifactWithoutHash,
140
- // Artifact hash is *very* expensive to compute, so we do it here once
141
- // and the TXE contract data provider can cache it
142
- artifactHash: await computeArtifactHash(artifactWithoutHash),
143
- };
144
- this.logger.debug(
145
- `Deploy ${
146
- artifact.name
147
- } with initializer ${initializer}(${decodedArgs}) and public keys hash ${publicKeysHash.toString()}`,
148
- );
149
- instance = await getContractInstanceFromInstantiationParams(artifact, {
150
- constructorArgs: decodedArgs,
151
- skipArgsDecoding: true,
152
- salt: Fr.ONE,
153
- publicKeys,
154
- constructorArtifact: initializer ? initializer : undefined,
155
- deployer: AztecAddress.ZERO,
156
- });
157
- TXEArtifactsCache.set(cacheKey, { artifact, instance });
158
- }
40
+ export const TXEForeignCallInputSchema: z.ZodType<TXEForeignCallInput> = zodFor<TXEForeignCallInput>()(
41
+ z.object({
42
+ // Nargo generates session_id as a u64, which may exceed Number.MAX_SAFE_INTEGER.
43
+ // Zod 4's `.int()` enforces the safe-integer bound, so we drop it here and only require
44
+ // the value to be a non-negative number (it is used solely as a Map key).
45
+ // eslint-disable-next-line camelcase
46
+ session_id: z.number().nonnegative(),
47
+ function: z.string() as z.ZodType<TXEOracleFunctionName>,
48
+ // eslint-disable-next-line camelcase
49
+ root_path: z.string(),
50
+ // eslint-disable-next-line camelcase
51
+ package_name: z.string(),
52
+ inputs: ForeignCallArgsSchema,
53
+ }),
54
+ );
55
+
56
+ export interface TXEDispatcherOptions {
57
+ /**
58
+ * Path to an LMDB directory holding the required protocol contracts (see
59
+ * {@link TXE_REQUIRED_PROTOCOL_CONTRACTS}) and the SchnorrAccount artifact. When set, the
60
+ * dispatcher clones this directory into a fresh tmpdir on first use instead of registering
61
+ * the contracts itself.
62
+ */
63
+ contractStoreSourceDir: string;
64
+ /**
65
+ * Class id (hex) of the SchnorrAccount artifact pre-registered in the shared LMDB. The
66
+ * {@link TXEArtifactResolver} looks the artifact up from the cloned store via this class id
67
+ * instead of recomputing it via `getSchnorrAccountContractArtifact()` + `computeArtifactHash()`.
68
+ */
69
+ schnorrClassId: string;
70
+ }
159
71
 
160
- inputs.splice(0, 1, artifact, instance, toSingle(secret));
72
+ export class TXEDispatcher {
73
+ private contractStore!: ContractStore;
74
+ private artifactResolver!: TXEArtifactResolver;
75
+ private readonly contractStoreSourceDir: string;
76
+ private readonly schnorrClassId: Fr;
77
+
78
+ constructor(
79
+ private logger: Logger,
80
+ opts: TXEDispatcherOptions,
81
+ ) {
82
+ this.contractStoreSourceDir = opts.contractStoreSourceDir;
83
+ this.schnorrClassId = Fr.fromString(opts.schnorrClassId);
161
84
  }
162
85
 
163
- async #processAddAccountInputs({ inputs }: TXEForeignCallInput) {
164
- const secret = fromSingle(inputs[0] as ForeignCallSingle);
165
-
166
- const cacheKey = `SchnorrAccountContract-${secret}`;
167
-
168
- let artifact: ContractArtifactWithHash;
169
- let instance;
170
-
171
- if (TXEArtifactsCache.has(cacheKey)) {
172
- this.logger.debug(`Using cached artifact for ${cacheKey}`);
173
- ({ artifact, instance } = TXEArtifactsCache.get(cacheKey)!);
174
- } else {
175
- const keys = await deriveKeys(secret);
176
- const args = [keys.publicKeys.masterIncomingViewingPublicKey.x, keys.publicKeys.masterIncomingViewingPublicKey.y];
177
- artifact = {
178
- ...SchnorrAccountContractArtifact,
179
- // Artifact hash is *very* expensive to compute, so we do it here once
180
- // and the TXE contract data provider can cache it
181
- artifactHash: await computeArtifactHash(SchnorrAccountContractArtifact),
182
- };
183
- instance = await getContractInstanceFromInstantiationParams(artifact, {
184
- constructorArgs: args,
185
- skipArgsDecoding: true,
186
- salt: Fr.ONE,
187
- publicKeys: keys.publicKeys,
188
- constructorArtifact: 'constructor',
189
- deployer: AztecAddress.ZERO,
190
- });
191
- TXEArtifactsCache.set(cacheKey, { artifact, instance });
86
+ /**
87
+ * Clones the pre-populated LMDB at `contractStoreSourceDir` into a fresh per-instance tmpdir
88
+ * on first use, so this dispatcher has a writable store already containing the required
89
+ * protocol contracts + SchnorrAccount. Idempotent — subsequent calls are no-ops.
90
+ */
91
+ private async warmUp(): Promise<void> {
92
+ if (this.contractStore) {
93
+ return;
192
94
  }
193
-
194
- inputs.splice(0, 0, artifact, instance);
95
+ const t0 = Date.now();
96
+ const kvStore = await cloneEphemeralStoreFrom(
97
+ join(this.contractStoreSourceDir, 'data.mdb'),
98
+ 'txe-contracts',
99
+ undefined,
100
+ 2,
101
+ );
102
+ this.contractStore = new ContractStore(kvStore);
103
+ this.artifactResolver = new TXEArtifactResolver(this.contractStore, this.schnorrClassId);
104
+ this.logger.debug('Cloned shared protocol-contracts store', { totalMs: Date.now() - t0 });
195
105
  }
196
106
 
197
107
  // eslint-disable-next-line camelcase
198
108
  async resolve_foreign_call(callData: TXEForeignCallInput): Promise<ForeignCallResult> {
199
- const { session_id: sessionId, function: functionName, inputs } = callData;
109
+ const {
110
+ session_id: sessionId,
111
+ function: functionName,
112
+ inputs,
113
+ root_path: rootPath,
114
+ package_name: packageName,
115
+ } = callData;
200
116
  this.logger.debug(`Calling ${functionName} on session ${sessionId}`);
201
117
 
202
118
  if (!sessions.has(sessionId)) {
203
119
  this.logger.debug(`Creating new session ${sessionId}`);
204
- if (!this.protocolContracts) {
205
- this.protocolContracts = await Promise.all(
206
- protocolContractNames.map(name => new BundledProtocolContractsProvider().getProtocolContractArtifact(name)),
207
- );
208
- }
209
- sessions.set(sessionId, await TXESession.init(this.protocolContracts));
210
- }
211
-
212
- switch (functionName) {
213
- case 'txeDeploy': {
214
- await this.#processDeployInputs(callData);
215
- break;
216
- }
217
- case 'txeAddAccount': {
218
- await this.#processAddAccountInputs(callData);
219
- break;
220
- }
120
+ await this.warmUp();
121
+ sessions.set(sessionId, await TXESession.init(this.contractStore, this.artifactResolver, rootPath, packageName));
221
122
  }
222
123
 
223
124
  return await sessions.get(sessionId)!.processFunction(functionName, inputs);
224
125
  }
126
+
127
+ /**
128
+ * Releases a session and its resources (per-session LMDB + `NativeWorldStateService`).
129
+ * Called by the dispatcher pool when nargo closes its TCP connection for a test (see
130
+ * `rpc_server.ts`'s socket tracker). No-op if the session was never created — that happens
131
+ * when nargo opens a connection but errors before sending a request.
132
+ */
133
+ async disposeSession(sessionId: number): Promise<void> {
134
+ const session = sessions.get(sessionId);
135
+ if (!session) {
136
+ return;
137
+ }
138
+ sessions.delete(sessionId);
139
+ await session.dispose();
140
+ }
225
141
  }
226
142
 
227
- const TXEDispatcherApiSchema: ApiSchemaFor<TXEDispatcher> = {
143
+ /** Diagnostic-only: number of sessions currently held by this worker. */
144
+ export function activeSessionCount(): number {
145
+ return sessions.size;
146
+ }
147
+
148
+ export const TXEDispatcherApiSchema: ApiSchemaFor<TXEDispatcher> = {
228
149
  // eslint-disable-next-line camelcase
229
- resolve_foreign_call: z.function().args(TXEForeignCallInputSchema).returns(ForeignCallResultSchema),
150
+ resolve_foreign_call: z.function({ input: z.tuple([TXEForeignCallInputSchema]), output: ForeignCallResultSchema }),
151
+ // disposeSession is invoked over IPC from the worker, not via RPC; required by ApiSchemaFor.
152
+ disposeSession: z.function({ input: z.tuple([z.number().nonnegative()]), output: z.void() }),
230
153
  };
231
-
232
- /**
233
- * Creates an RPC server that forwards calls to the TXE.
234
- * @param logger - Logger to output to
235
- * @returns A TXE RPC server.
236
- */
237
- export function createTXERpcServer(logger: Logger) {
238
- return createSafeJsonRpcServer(new TXEDispatcher(logger), TXEDispatcherApiSchema, {
239
- http200OnError: true,
240
- });
241
- }