@aztec/txe 5.0.0-private.20260318 → 5.0.0-rc.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 (154) hide show
  1. package/dest/AuthRegistry-CPGFQR26.js +3 -0
  2. package/dest/AuthRegistry-CPGFQR26.js.map +7 -0
  3. package/dest/ContractClassRegistry-EHVIHGEK.js +3 -0
  4. package/dest/ContractClassRegistry-EHVIHGEK.js.map +7 -0
  5. package/dest/ContractInstanceRegistry-DWZDXHRG.js +3 -0
  6. package/dest/ContractInstanceRegistry-DWZDXHRG.js.map +7 -0
  7. package/dest/FeeJuice-MI32ZO7B.js +3 -0
  8. package/dest/FeeJuice-MI32ZO7B.js.map +7 -0
  9. package/dest/HandshakeRegistry-3KSP3ITH.js +3 -0
  10. package/dest/HandshakeRegistry-3KSP3ITH.js.map +7 -0
  11. package/dest/MultiCallEntrypoint-IU7HYFYE.js +3 -0
  12. package/dest/MultiCallEntrypoint-IU7HYFYE.js.map +7 -0
  13. package/dest/SchnorrAccount-6TUE7JX4.js +3 -0
  14. package/dest/SchnorrAccount-6TUE7JX4.js.map +7 -0
  15. package/dest/SchnorrInitializerlessAccount-S3DU2DJK.js +3 -0
  16. package/dest/SchnorrInitializerlessAccount-S3DU2DJK.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 +61 -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-5U25VAFR.js +265 -0
  26. package/dest/chunk-5U25VAFR.js.map +7 -0
  27. package/dest/chunk-BJVAAXNA.js +3 -0
  28. package/dest/chunk-BJVAAXNA.js.map +7 -0
  29. package/dest/chunk-UPW55EJX.js +304 -0
  30. package/dest/chunk-UPW55EJX.js.map +7 -0
  31. package/dest/constants.d.ts +5 -1
  32. package/dest/constants.d.ts.map +1 -1
  33. package/dest/constants.js +8 -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 +70 -190
  40. package/dest/metafile.json +38829 -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 +33 -8
  45. package/dest/oracle/interfaces.d.ts.map +1 -1
  46. package/dest/oracle/test-resolver/fixtures.d.ts +43 -0
  47. package/dest/oracle/test-resolver/fixtures.d.ts.map +1 -0
  48. package/dest/oracle/test-resolver/fixtures.js +39 -0
  49. package/dest/oracle/test-resolver/index.d.ts +9 -0
  50. package/dest/oracle/test-resolver/index.d.ts.map +1 -0
  51. package/dest/oracle/test-resolver/index.js +33 -0
  52. package/dest/oracle/test-resolver/resolver.d.ts +34 -0
  53. package/dest/oracle/test-resolver/resolver.d.ts.map +1 -0
  54. package/dest/oracle/test-resolver/resolver.js +114 -0
  55. package/dest/oracle/txe_oracle_public_context.d.ts +26 -2
  56. package/dest/oracle/txe_oracle_public_context.d.ts.map +1 -1
  57. package/dest/oracle/txe_oracle_public_context.js +43 -1
  58. package/dest/oracle/txe_oracle_registry.d.ts +14 -0
  59. package/dest/oracle/txe_oracle_registry.d.ts.map +1 -0
  60. package/dest/oracle/txe_oracle_registry.js +562 -0
  61. package/dest/oracle/txe_oracle_top_level_context.d.ts +32 -18
  62. package/dest/oracle/txe_oracle_top_level_context.d.ts.map +1 -1
  63. package/dest/oracle/txe_oracle_top_level_context.js +151 -55
  64. package/dest/oracle/txe_oracle_version.d.ts +17 -0
  65. package/dest/oracle/txe_oracle_version.d.ts.map +1 -0
  66. package/dest/oracle/txe_oracle_version.js +14 -0
  67. package/dest/oracle/txe_private_execution_oracle.d.ts +17 -0
  68. package/dest/oracle/txe_private_execution_oracle.d.ts.map +1 -0
  69. package/dest/oracle/txe_private_execution_oracle.js +15 -0
  70. package/dest/rpc_server.d.ts +14 -0
  71. package/dest/rpc_server.d.ts.map +1 -0
  72. package/dest/rpc_server.js +78 -0
  73. package/dest/rpc_translator.d.ts +103 -230
  74. package/dest/rpc_translator.d.ts.map +1 -1
  75. package/dest/rpc_translator.js +697 -616
  76. package/dest/server.bundle.js +3 -0
  77. package/dest/server.bundle.js.map +7 -0
  78. package/dest/state_machine/archiver.d.ts +4 -3
  79. package/dest/state_machine/archiver.d.ts.map +1 -1
  80. package/dest/state_machine/archiver.js +26 -15
  81. package/dest/state_machine/dummy_p2p_client.d.ts +14 -7
  82. package/dest/state_machine/dummy_p2p_client.d.ts.map +1 -1
  83. package/dest/state_machine/dummy_p2p_client.js +19 -4
  84. package/dest/state_machine/global_variable_builder.d.ts +9 -4
  85. package/dest/state_machine/global_variable_builder.d.ts.map +1 -1
  86. package/dest/state_machine/global_variable_builder.js +9 -3
  87. package/dest/state_machine/index.d.ts +4 -2
  88. package/dest/state_machine/index.d.ts.map +1 -1
  89. package/dest/state_machine/index.js +11 -3
  90. package/dest/state_machine/mock_epoch_cache.d.ts +16 -3
  91. package/dest/state_machine/mock_epoch_cache.d.ts.map +1 -1
  92. package/dest/state_machine/mock_epoch_cache.js +29 -2
  93. package/dest/state_machine/synchronizer.js +1 -1
  94. package/dest/txe_session.d.ts +85 -17
  95. package/dest/txe_session.d.ts.map +1 -1
  96. package/dest/txe_session.js +245 -40
  97. package/dest/utils/encoding.d.ts +191 -0
  98. package/dest/utils/encoding.d.ts.map +1 -0
  99. package/dest/{util → utils}/encoding.js +7 -2
  100. package/dest/{util → utils}/expected_failure_error.d.ts +1 -1
  101. package/dest/utils/expected_failure_error.d.ts.map +1 -0
  102. package/dest/{util → utils}/txe_account_store.d.ts +1 -1
  103. package/dest/utils/txe_account_store.d.ts.map +1 -0
  104. package/dest/utils/txe_artifact_resolver.d.ts +37 -0
  105. package/dest/utils/txe_artifact_resolver.d.ts.map +1 -0
  106. package/dest/utils/txe_artifact_resolver.js +161 -0
  107. package/dest/utils/txe_public_contract_data_source.d.ts +20 -0
  108. package/dest/utils/txe_public_contract_data_source.d.ts.map +1 -0
  109. package/dest/{util → utils}/txe_public_contract_data_source.js +1 -3
  110. package/dest/worker.bundle.js +3 -0
  111. package/dest/worker.bundle.js.map +7 -0
  112. package/dest/worker.d.ts +2 -0
  113. package/dest/worker.d.ts.map +1 -0
  114. package/dest/worker.js +92 -0
  115. package/package.json +38 -21
  116. package/src/bin/check_txe_oracle_version.ts +70 -0
  117. package/src/bin/index.ts +11 -2
  118. package/src/bin/oracle_test_server.ts +51 -0
  119. package/src/constants.ts +10 -0
  120. package/src/dispatcher_pool.ts +317 -0
  121. package/src/index.ts +97 -227
  122. package/src/msgpackr_fr_extension.ts +23 -0
  123. package/src/oracle/interfaces.ts +29 -7
  124. package/src/oracle/test-resolver/fixtures.ts +84 -0
  125. package/src/oracle/test-resolver/index.ts +45 -0
  126. package/src/oracle/test-resolver/resolver.ts +165 -0
  127. package/src/oracle/txe_oracle_public_context.ts +60 -0
  128. package/src/oracle/txe_oracle_registry.ts +401 -0
  129. package/src/oracle/txe_oracle_top_level_context.ts +185 -64
  130. package/src/oracle/txe_oracle_version.ts +17 -0
  131. package/src/oracle/txe_private_execution_oracle.ts +30 -0
  132. package/src/rpc_server.ts +87 -0
  133. package/src/rpc_translator.ts +767 -892
  134. package/src/state_machine/archiver.ts +38 -16
  135. package/src/state_machine/dummy_p2p_client.ts +35 -11
  136. package/src/state_machine/global_variable_builder.ts +18 -3
  137. package/src/state_machine/index.ts +17 -5
  138. package/src/state_machine/mock_epoch_cache.ts +38 -3
  139. package/src/state_machine/synchronizer.ts +1 -1
  140. package/src/txe_session.ts +437 -50
  141. package/src/{util → utils}/encoding.ts +8 -2
  142. package/src/utils/txe_artifact_resolver.ts +217 -0
  143. package/src/{util → utils}/txe_public_contract_data_source.ts +0 -2
  144. package/src/worker.ts +98 -0
  145. package/dest/util/encoding.d.ts +0 -720
  146. package/dest/util/encoding.d.ts.map +0 -1
  147. package/dest/util/expected_failure_error.d.ts.map +0 -1
  148. package/dest/util/txe_account_store.d.ts.map +0 -1
  149. package/dest/util/txe_public_contract_data_source.d.ts +0 -20
  150. package/dest/util/txe_public_contract_data_source.d.ts.map +0 -1
  151. /package/dest/{util → utils}/expected_failure_error.js +0 -0
  152. /package/dest/{util → utils}/txe_account_store.js +0 -0
  153. /package/src/{util → utils}/expected_failure_error.ts +0 -0
  154. /package/src/{util → utils}/txe_account_store.ts +0 -0
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@aztec/txe",
3
- "version": "5.0.0-private.20260318",
3
+ "version": "5.0.0-rc.1",
4
4
  "type": "module",
5
- "exports": "./dest/index.js",
5
+ "exports": {
6
+ "./server": {
7
+ "types": "./dest/rpc_server.d.ts",
8
+ "import": "./dest/server.bundle.js"
9
+ }
10
+ },
6
11
  "bin": "./dest/bin/index.js",
7
12
  "typedocOptions": {
8
13
  "entryPoints": [
@@ -12,15 +17,18 @@
12
17
  "tsconfig": "./tsconfig.json"
13
18
  },
14
19
  "scripts": {
15
- "build": "yarn clean && ../scripts/tsc.sh",
20
+ "build": "yarn clean && ../scripts/tsc.sh && node ./esbuild.config.mjs",
16
21
  "build:dev": "../scripts/tsc.sh --watch",
17
22
  "clean": "rm -rf ./dest .tsbuildinfo",
18
23
  "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}",
19
24
  "dev": "LOG_LEVEL=\"debug; trace: simulator:state_manager; info: json-rpc:proxy\" node ./dest/bin/index.js",
20
- "start": "node --no-warnings ./dest/bin/index.js"
25
+ "start": "node --no-warnings ./dest/bin/index.js",
26
+ "start:oracle-test-resolver": "node --no-warnings ./dest/bin/oracle_test_server.js",
27
+ "check_txe_oracle_version": "node ./dest/bin/check_txe_oracle_version.js"
21
28
  },
22
29
  "inherits": [
23
- "../package.common.json"
30
+ "../package.common.json",
31
+ "./package.local.json"
24
32
  ],
25
33
  "jest": {
26
34
  "moduleNameMapper": {
@@ -61,38 +69,47 @@
61
69
  ]
62
70
  },
63
71
  "dependencies": {
64
- "@aztec/accounts": "5.0.0-private.20260318",
65
- "@aztec/archiver": "5.0.0-private.20260318",
66
- "@aztec/aztec-node": "5.0.0-private.20260318",
67
- "@aztec/aztec.js": "5.0.0-private.20260318",
68
- "@aztec/bb-prover": "5.0.0-private.20260318",
69
- "@aztec/constants": "5.0.0-private.20260318",
70
- "@aztec/foundation": "5.0.0-private.20260318",
71
- "@aztec/key-store": "5.0.0-private.20260318",
72
- "@aztec/kv-store": "5.0.0-private.20260318",
73
- "@aztec/protocol-contracts": "5.0.0-private.20260318",
74
- "@aztec/pxe": "5.0.0-private.20260318",
75
- "@aztec/simulator": "5.0.0-private.20260318",
76
- "@aztec/stdlib": "5.0.0-private.20260318",
77
- "@aztec/world-state": "5.0.0-private.20260318",
78
- "zod": "^3.23.8"
72
+ "@aztec/accounts": "5.0.0-rc.1",
73
+ "@aztec/archiver": "5.0.0-rc.1",
74
+ "@aztec/aztec-node": "5.0.0-rc.1",
75
+ "@aztec/aztec.js": "5.0.0-rc.1",
76
+ "@aztec/bb-prover": "5.0.0-rc.1",
77
+ "@aztec/bb.js": "5.0.0-rc.1",
78
+ "@aztec/constants": "5.0.0-rc.1",
79
+ "@aztec/foundation": "5.0.0-rc.1",
80
+ "@aztec/key-store": "5.0.0-rc.1",
81
+ "@aztec/kv-store": "5.0.0-rc.1",
82
+ "@aztec/protocol-contracts": "5.0.0-rc.1",
83
+ "@aztec/pxe": "5.0.0-rc.1",
84
+ "@aztec/simulator": "5.0.0-rc.1",
85
+ "@aztec/standard-contracts": "5.0.0-rc.1",
86
+ "@aztec/stdlib": "5.0.0-rc.1",
87
+ "@aztec/world-state": "5.0.0-rc.1",
88
+ "msgpackr": "^1.11.2",
89
+ "zod": "^4"
79
90
  },
80
91
  "devDependencies": {
81
92
  "@jest/globals": "^30.0.0",
82
93
  "@types/jest": "^30.0.0",
83
94
  "@types/node": "^22.15.17",
84
95
  "@typescript/native-preview": "7.0.0-dev.20260113.1",
96
+ "esbuild": "^0.27.0",
85
97
  "jest": "^30.0.0",
86
98
  "jest-mock-extended": "^4.0.0",
87
99
  "ts-node": "^10.9.1",
88
100
  "typescript": "^5.3.3"
89
101
  },
90
102
  "files": [
103
+ "dest/bin/index.js",
104
+ "dest/worker.bundle.js",
105
+ "dest/server.bundle.js",
106
+ "dest/chunk-*.js",
107
+ "dest/*-*.js",
108
+ "dest/rpc_server.d.ts",
91
109
  "dest",
92
110
  "src",
93
111
  "!*.test.*"
94
112
  ],
95
- "types": "./dest/index.d.ts",
96
113
  "engines": {
97
114
  "node": ">=20.10"
98
115
  }
@@ -0,0 +1,70 @@
1
+ import { keccak256String } from '@aztec/foundation/crypto/keccak';
2
+ import { getOracleInterfaceSignature, readNumericGlobal } from '@aztec/pxe/bin';
3
+
4
+ import { dirname, join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ import { TXE_ORACLE_INTERFACE_HASH, TXE_ORACLE_VERSION_MAJOR } from '../oracle/txe_oracle_version.js';
8
+
9
+ /**
10
+ * Verifies that the TXE oracle interfaces match the expected interface hash.
11
+ *
12
+ * The TXE oracle interfaces need to be versioned to ensure compatibility between Aztec.nr tests and TXE. This function
13
+ * computes a hash of the TXE oracle interfaces and compares it against a known hash. If they don't match, it means an
14
+ * interface has changed and the TXE oracle version needs to be bumped:
15
+ * - If the change is backward-breaking (e.g. removing/renaming an oracle), bump TXE_ORACLE_VERSION_MAJOR.
16
+ * - If the change is an oracle addition (non-breaking), bump TXE_ORACLE_VERSION_MINOR.
17
+ */
18
+ function assertTxeOracleInterfaceMatches(): void {
19
+ const currentDir = dirname(fileURLToPath(import.meta.url));
20
+ const packageRoot = dirname(dirname(currentDir));
21
+ const interfacesSourcePath = join(packageRoot, 'src/oracle/interfaces.ts');
22
+
23
+ const targets = ['IAvmExecutionOracle', 'ITxeExecutionOracle'];
24
+ // Not an oracle foreign call handler (see TODO(F-335) in interfaces.ts).
25
+ const excludedMembers = ['syncContractNonOracleMethod'];
26
+
27
+ const txeOracleInterfaceSignature = getOracleInterfaceSignature(interfacesSourcePath, targets, excludedMembers);
28
+
29
+ const txeOracleInterfaceHash = keccak256String(txeOracleInterfaceSignature);
30
+ if (txeOracleInterfaceHash !== TXE_ORACLE_INTERFACE_HASH) {
31
+ throw new Error(
32
+ `The TXE oracle interface has changed. Update TXE_ORACLE_INTERFACE_HASH to ${txeOracleInterfaceHash} in txe/src/oracle/txe_oracle_version.ts and bump the TXE oracle version (TXE_ORACLE_VERSION_MAJOR for breaking changes, TXE_ORACLE_VERSION_MINOR for oracle additions).`,
33
+ );
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Verifies that `TXE_ORACLE_VERSION_MAJOR` is identical across its two hand-maintained copies: the TXE TypeScript
39
+ * constant and the Aztec.nr Noir test helper constant. These layers can't import each other, so the major version is
40
+ * duplicated by hand and a mismatch otherwise only surfaces at runtime. This is the TXE counterpart of the contract
41
+ * oracle version check in `pxe/src/bin/check_oracle_version.ts`; the two version families are tracked separately.
42
+ *
43
+ * Only the major version is checked: the minor version legitimately diverges under the "environment minor >= contract
44
+ * minor" tolerance, so asserting minor equality would false-positive.
45
+ */
46
+ function assertTxeOracleVersionMajorInSync(): void {
47
+ const currentDir = dirname(fileURLToPath(import.meta.url));
48
+ // Go up from dest/bin/ or src/bin/ to the package root (txe/), then up two more to the git root.
49
+ const packageRoot = dirname(dirname(currentDir));
50
+ const gitRoot = join(packageRoot, '..', '..');
51
+
52
+ const copies = [
53
+ { label: 'txe/src/txe_oracle_version.ts', value: TXE_ORACLE_VERSION_MAJOR },
54
+ {
55
+ label: 'aztec-nr/aztec/src/test/helpers/txe_oracles.nr',
56
+ value: readNumericGlobal(
57
+ join(gitRoot, 'noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr'),
58
+ 'TXE_ORACLE_VERSION_MAJOR',
59
+ ),
60
+ },
61
+ ];
62
+
63
+ if (new Set(copies.map(copy => copy.value)).size > 1) {
64
+ const details = copies.map(copy => `${copy.label}=${copy.value}`).join(', ');
65
+ throw new Error(`TXE_ORACLE_VERSION_MAJOR is out of sync: ${details}. Bump all copies together.`);
66
+ }
67
+ }
68
+
69
+ assertTxeOracleInterfaceMatches();
70
+ assertTxeOracleVersionMajorInSync();
package/src/bin/index.ts CHANGED
@@ -2,7 +2,16 @@
2
2
  import { createLogger } from '@aztec/aztec.js/log';
3
3
  import { startHttpRpcServer } from '@aztec/foundation/json-rpc/server';
4
4
 
5
- import { createTXERpcServer } from '../index.js';
5
+ import { createTXERpcServer } from '../rpc_server.js';
6
+
7
+ // Cap the native world-state thread pool before any import touches @aztec/world-state.
8
+ // `MAX_WORLD_STATE_THREADS` reads HARDWARE_CONCURRENCY at module load and defaults to 16; the
9
+ // pool's worker threads inherit `process.env`, so setting the cap here covers every worker.
10
+ //
11
+ // CAVEAT: HARDWARE_CONCURRENCY is process-global — bb prove/verify, the LMDB reader pool, and
12
+ // the world-state native thread pool all read it. Safe at 2 today because TXE never proves or
13
+ // verifies and only performs light tree updates; raise it if that ever changes.
14
+ process.env.HARDWARE_CONCURRENCY ??= '2';
6
15
 
7
16
  /**
8
17
  * Create and start a new TXE HTTP Server
@@ -23,7 +32,7 @@ async function main() {
23
32
  const logger = createLogger('txe:rpc');
24
33
  logger.info(`Setting up TXE...`);
25
34
 
26
- const txeServer = createTXERpcServer(logger);
35
+ const txeServer = await createTXERpcServer(logger);
27
36
  const { port } = await startHttpRpcServer(txeServer, {
28
37
  host: '127.0.0.1',
29
38
  port: TXE_PORT,
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env -S node --no-warnings
2
+ import { createLogger } from '@aztec/aztec.js/log';
3
+ import { startHttpRpcServer } from '@aztec/foundation/json-rpc/server';
4
+
5
+ import { createOracleTestRpcServer } from '../oracle/test-resolver/index.js';
6
+
7
+ /**
8
+ * Starts an HTTP RPC server that resolves oracle foreign calls using hardcoded fixture scenarios.
9
+ * Used by `nargo test --oracle-resolver` to run `#[oracle_test]` roundtrip tests with a dedicated oracle resolver.
10
+ * Logs fixture coverage on shutdown.
11
+ */
12
+ async function main() {
13
+ const { ORACLE_TEST_PORT = 14830 } = process.env;
14
+
15
+ const logger = createLogger('txe:oracle-test');
16
+
17
+ logger.info('Setting up oracle test resolver...');
18
+ const { server, resolver } = createOracleTestRpcServer(logger);
19
+
20
+ function logCoverageAndExit() {
21
+ const uncalled = resolver.getUncalledFixtures();
22
+ const missing = resolver.getMissingFixtures();
23
+ if (uncalled.length > 0) {
24
+ logger.warn(`Fixtures never called by any test: ${uncalled.join(', ')}`);
25
+ }
26
+ if (missing.length > 0) {
27
+ logger.debug(`Oracles with no fixture defined: ${missing.join(', ')}`);
28
+ }
29
+ if (uncalled.length === 0) {
30
+ logger.info('All fixture oracles were called.');
31
+ }
32
+ process.exit(0);
33
+ }
34
+
35
+ process.on('SIGTERM', logCoverageAndExit);
36
+ process.on('SIGINT', logCoverageAndExit);
37
+
38
+ const { port } = await startHttpRpcServer(server, {
39
+ host: '127.0.0.1',
40
+ port: ORACLE_TEST_PORT,
41
+ timeoutMs: 1e3 * 60 * 5,
42
+ });
43
+
44
+ logger.info(`Oracle test resolver listening on port ${port}`);
45
+ }
46
+
47
+ main().catch(err => {
48
+ // eslint-disable-next-line no-console
49
+ console.error(err);
50
+ process.exit(1);
51
+ });
package/src/constants.ts CHANGED
@@ -1,3 +1,13 @@
1
+ import { PRIVATE_LOG_CIPHERTEXT_LEN } from '@aztec/constants';
1
2
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
2
3
 
3
4
  export const DEFAULT_ADDRESS = AztecAddress.fromNumber(42);
5
+
6
+ // This is MAX_MESSAGE_CONTENT_LEN - PRIVATE_EVENT_MSG_PLAINTEXT_RESERVED_FIELDS_LEN
7
+ export const MAX_PRIVATE_EVENT_LEN = 10;
8
+ export const MAX_PRIVATE_EVENTS_PER_TXE_QUERY = 5;
9
+
10
+ // Arbitrarily set at 64 because we need a bound. Nothing inherent about it.
11
+ export const MAX_OFFCHAIN_EFFECTS_PER_TXE_QUERY = 64;
12
+ // Must match MAX_OFFCHAIN_EFFECT_LEN in noir-projects/aztec-nr/aztec/src/test/helpers/txe_oracles.nr.
13
+ export const MAX_OFFCHAIN_EFFECT_LEN = 2 + PRIVATE_LOG_CIPHERTEXT_LEN;
@@ -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
+ }