@tokamak-private-dapps/private-state-cli 0.1.6 → 0.1.9

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9 - 2026-05-03
4
+
5
+ - Used the bundled Groth16 package version as the default `private-state-cli --install` Groth16 runtime version.
6
+ - Treated stale local Groth16 CRS metadata without `compatibleBackendVersion` as a cache miss so the matching public CRS can be reinstalled.
7
+
8
+ ## 0.1.8 - 2026-04-30
9
+
10
+ - Reused common proof backend version helpers for Tokamak and Groth16 compatibility checks.
11
+ - Reused common npm registry metadata lookup during proof backend runtime installation.
12
+
13
+ ## 0.1.7 - 2026-04-29
14
+
15
+ - Required Groth16 channel verifier and installed CRS compatibility versions to use canonical major.minor form.
16
+ - Matched Groth16 channel verifier compatibility against the installed CRS major.minor compatibility version.
17
+ - Required the Groth16 package version with verified public CRS archive selection.
18
+ - Required Tokamak zk-EVM channel verifier and CLI package compatibility versions to use canonical major.minor form.
19
+
3
20
  ## 0.1.6 - 2026-04-29
4
21
 
5
22
  - Added `--groth16-cli-version` and `--tokamak-zk-evm-cli-version` install options with npm latest defaults.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Command-line client for the Tokamak private-state DApp.
4
4
 
5
+ The full private-state DApp documentation is published with the repository:
6
+
7
+ - https://github.com/tokamak-network/Tokamak-zk-EVM-contracts/tree/main/packages/apps/private-state/docs
8
+
5
9
  ## Install
6
10
 
7
11
  ```bash
@@ -15,14 +19,18 @@ artifacts:
15
19
  private-state-cli --install
16
20
  ```
17
21
 
18
- By default, `--install` resolves the latest `@tokamak-zk-evm/cli` and `@tokamak-private-dapps/groth16` versions from
19
- the npm registry. To pin exact proof backend versions for a channel, pass explicit versions:
22
+ By default, `--install` resolves the latest `@tokamak-zk-evm/cli` from the npm registry and uses the bundled
23
+ `@tokamak-private-dapps/groth16` dependency version selected by the installed private-state CLI package. To pin exact
24
+ proof backend versions for a channel, pass explicit versions:
20
25
 
21
26
  ```bash
22
- private-state-cli --install --tokamak-zk-evm-cli-version 2.0.8 --groth16-cli-version 0.1.1
27
+ private-state-cli --install --tokamak-zk-evm-cli-version 2.1.0 --groth16-cli-version 0.2.0
23
28
  ```
24
29
 
25
- The Groth16 installer downloads the public Google Drive CRS archive with the same version as the selected Groth16 CLI.
30
+ The Groth16 installer downloads the public Google Drive CRS archive whose major.minor compatibility version matches the
31
+ selected Groth16 CLI package version.
32
+ The Tokamak zk-EVM installer requires the selected CLI package to declare
33
+ `tokamakZkEvm.compatibleBackendVersion` as a canonical major.minor version matching the selected package version.
26
34
 
27
35
  `--install` downloads public deployment artifacts from the configured artifact index. It does not read repository-local
28
36
  `deployment/` outputs by default. Repository development workflows that need local anvil artifacts can opt in explicitly:
@@ -60,6 +68,19 @@ A common private-state flow is:
60
68
 
61
69
  Use `private-state-cli --help` for the full command list and required options.
62
70
 
71
+ Channel policy warning:
72
+
73
+ - `create-channel` commits to an immutable channel policy: verifier bindings, DApp execution metadata, function layout,
74
+ managed storage vector, and refund policy are fixed for that channel.
75
+ - `join-channel` means the user accepts the channel's current policy. Later policy-level fixes require a new channel or
76
+ migration; the existing channel is intentionally not mutated in place without renewed user consent.
77
+ - Before sending a channel-creation transaction or a first channel-registration transaction, the CLI prints the policy
78
+ snapshot that will be accepted: DApp metadata digest, digest schema, Groth16 verifier address, Groth16 compatible
79
+ backend version, Tokamak verifier address, and Tokamak compatible backend version.
80
+ - Users and operators must review this snapshot before signing. If any digest, schema, verifier address, or compatible
81
+ backend version is unexpected or has not been reviewed, do not create or join the channel. A later correction creates
82
+ a new channel; it does not rewrite the policy of an already-created channel.
83
+
63
84
  `private-state-cli --doctor` reports the CLI package version, dependency versions recorded by the last
64
85
  `private-state-cli --install`, selected proof backend runtime versions, current dependency versions through `tokamak-l2js`, and Tokamak zk-EVM runtime
65
86
  install mode, Docker mode, CUDA runtime metadata, live `nvidia-smi` and Docker GPU probe results, and Groth16
@@ -114,6 +135,9 @@ Operating rules:
114
135
  telling the user to move funds.
115
136
  - Explain that wallet names are local CLI identifiers, while private transfers use notes owned by L2 addresses
116
137
  registered in the channel.
138
+ - Before guiding a user through `create-channel` or `join-channel`, explain that channel policy is immutable after
139
+ creation and that joining a channel means accepting its current verifier, DApp metadata, function layout, managed
140
+ storage vector, and refund policy.
117
141
  - Do not present one fixed command sequence as universally correct. Some flows start from an existing channel or wallet,
118
142
  while others require creating or joining a channel first.
119
143
  - When the user asks for a transfer, first determine whether the sender has minted notes available. If not, guide them
@@ -147,6 +171,8 @@ using bridge-facing commands on a new machine.
147
171
  Channel balance commands such as `deposit-channel` and `withdraw-channel` use the installed Groth16 runtime workspace
148
172
  directly. Proof generation writes to the fixed workspace paths under `~/tokamak-private-channels/groth16/proof`; the CLI
149
173
  does not pass custom `--zkey`, proof-output, or public-output paths to the Groth16 prover.
174
+ Before proof generation, the CLI compares the target channel's verifier compatibility versions with the installed
175
+ Tokamak zk-EVM and Groth16 major.minor compatibility versions.
150
176
 
151
177
  Release order matters for npm publication. `@tokamak-private-dapps/common-library` and
152
178
  `@tokamak-private-dapps/groth16` must be published before this package version.
@@ -400,6 +400,11 @@
400
400
  case, you lose ownership of all notes because you can no longer use them, and that ownership cannot be
401
401
  recovered.
402
402
  </li>
403
+ <li>
404
+ Channel policy is immutable after creation. Joining a channel means accepting its verifier bindings,
405
+ DApp metadata, function layout, managed storage vector, and refund policy; later policy-level fixes
406
+ require a new channel or migration.
407
+ </li>
403
408
  </ul>
404
409
  </div>
405
410
  </section>
@@ -521,12 +526,12 @@
521
526
  const commands = [
522
527
  { id: "install-zk-evm", description: "Install the local Tokamak zk-EVM toolchain. Optionally forward --docker for Linux-host Docker installs.", fields: ["docker"] },
523
528
  { id: "uninstall-zk-evm", description: "Remove the Tokamak zk-EVM CLI runtime workspace.", fields: [] },
524
- { id: "create-channel", description: "Create the bridge channel and initialize its workspace.", fields: ["channelName", "network", "privateKey", "alchemyApiKey"] },
529
+ { id: "create-channel", description: "Create a channel with an immutable operating policy and initialize its workspace.", fields: ["channelName", "network", "privateKey", "alchemyApiKey"] },
525
530
  { id: "recover-workspace", description: "Rebuild the saved channel workspace from bridge state.", fields: ["channelName", "network", "alchemyApiKey"] },
526
531
  { id: "deposit-bridge", description: "Deposit canonical tokens into the shared bridge vault.", fields: ["amount", "network", "privateKey", "alchemyApiKey"] },
527
532
  { id: "withdraw-bridge", description: "Withdraw shared bridge-vault funds back to the L1 wallet.", fields: ["amount", "network", "privateKey", "alchemyApiKey"] },
528
533
  { id: "get-my-bridge-fund", description: "Read the current bridge-vault balance.", fields: ["network", "privateKey", "alchemyApiKey"] },
529
- { id: "join-channel", description: "Bind the caller to a channel-specific L2 identity.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
534
+ { id: "join-channel", description: "Accept the channel policy and bind the caller to a channel-specific L2 identity.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
530
535
  { id: "recover-wallet", description: "Rebuild the recoverable portion of a wallet.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
531
536
  { id: "get-my-wallet-meta", description: "Check whether a saved wallet matches on-chain registration.", fields: ["wallet", "password", "network"] },
532
537
  { id: "get-my-l1-address", description: "Derive the L1 address for a private key.", fields: ["privateKey"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.9",
4
4
  "description": "Command-line client for the Tokamak private-state DApp.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "Tokamak Network",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@ethereumjs/util": "^10.1.1",
44
- "@noble/curves": "^1.2.0",
45
- "@tokamak-private-dapps/common-library": "^0.1.0",
46
- "@tokamak-private-dapps/groth16": "^0.1.2",
47
- "@tokamak-zk-evm/cli": "^2.0.12",
44
+ "@noble/curves": "1.9.7",
45
+ "@tokamak-private-dapps/common-library": "^0.1.1",
46
+ "@tokamak-private-dapps/groth16": "^0.2.0",
47
+ "@tokamak-zk-evm/cli": "^2.1.0",
48
48
  "ethers": "^6.14.1",
49
- "tokamak-l2js": "^0.1.3"
49
+ "tokamak-l2js": "^0.1.4"
50
50
  },
51
51
  "engines": {
52
52
  "node": ">=18"
@@ -44,6 +44,13 @@ import {
44
44
  hexToBytes,
45
45
  } from "@ethereumjs/util";
46
46
  import { deriveRpcUrl, resolveCliNetwork } from "@tokamak-private-dapps/common-library/network-config";
47
+ import { fetchNpmPackageMetadata } from "@tokamak-private-dapps/common-library/npm-registry";
48
+ import {
49
+ normalizePackageVersionToCompatibleBackendVersion,
50
+ readTokamakZkEvmCompatibleBackendVersionFromPackageJson,
51
+ requireCanonicalCompatibleBackendVersion,
52
+ requireExactSemverVersion,
53
+ } from "@tokamak-private-dapps/common-library/proof-backend-versioning";
47
54
  import {
48
55
  resolveTokamakBlockInputConfig,
49
56
  resolveTokamakCliEntryPath,
@@ -65,6 +72,8 @@ import {
65
72
  PUBLIC_GROTH16_MPC_DRIVE_FOLDER_ID,
66
73
  downloadLatestPublicGroth16MpcArtifacts,
67
74
  downloadPublicGroth16MpcArtifactsByVersion,
75
+ readGroth16CompatibleBackendVersionFromPackageJson,
76
+ requireCanonicalGroth16CompatibleBackendVersion,
68
77
  } from "@tokamak-private-dapps/groth16/public-drive-crs";
69
78
  import {
70
79
  CHANNEL_BOUND_L2_DERIVATION_MODE,
@@ -157,6 +166,68 @@ const DEFAULT_LOG_REQUESTS_PER_SECOND = 5;
157
166
  const LOG_REQUEST_INTERVAL_MS = Math.ceil(1000 / DEFAULT_LOG_REQUESTS_PER_SECOND);
158
167
  let lastLogRequestStartedAtMs = 0;
159
168
 
169
+ function printImmutableChannelPolicyWarning({
170
+ action,
171
+ channelName,
172
+ channelId,
173
+ channelManager = null,
174
+ policySnapshot = null,
175
+ }) {
176
+ const details = [
177
+ `WARNING: ${action} commits to an immutable channel policy.`,
178
+ `Channel: ${channelName} (${channelId.toString()})`,
179
+ ];
180
+ if (channelManager) {
181
+ details.push(`ChannelManager: ${channelManager}`);
182
+ }
183
+ details.push(
184
+ "The channel verifier bindings, DApp execution metadata, function layout, managed storage vector, and refund policy are fixed for this channel.",
185
+ "Those policy fields are intentionally not upgraded in place without channel-user consent.",
186
+ "If a policy bug is discovered later, the expected mitigation is creating or joining a new channel, not mutating this channel.",
187
+ "Review the DApp digest, digest schema, verifier addresses, and compatible backend versions before signing.",
188
+ );
189
+ if (policySnapshot) {
190
+ details.push(
191
+ "Channel policy snapshot:",
192
+ ` DApp id: ${policySnapshot.dappId}`,
193
+ ` DApp metadata digest schema: ${policySnapshot.dappMetadataDigestSchema}`,
194
+ ` DApp metadata digest: ${policySnapshot.dappMetadataDigest}`,
195
+ ` DApp function root: ${policySnapshot.functionRoot}`,
196
+ ` Groth16 verifier: ${policySnapshot.grothVerifier}`,
197
+ ` Groth16 compatible backend version: ${policySnapshot.grothVerifierCompatibleBackendVersion}`,
198
+ ` Tokamak verifier: ${policySnapshot.tokamakVerifier}`,
199
+ ` Tokamak compatible backend version: ${policySnapshot.tokamakVerifierCompatibleBackendVersion}`,
200
+ "Do not sign if any snapshot value is unexpected or has not been reviewed.",
201
+ );
202
+ }
203
+ console.error(details.join("\n"));
204
+ }
205
+
206
+ function normalizeDAppPolicySnapshot({
207
+ dappId,
208
+ metadataDigest,
209
+ metadataDigestSchema,
210
+ functionRoot,
211
+ verifierSnapshot,
212
+ }) {
213
+ return {
214
+ dappId: Number(dappId),
215
+ dappMetadataDigestSchema: normalizeBytes32Hex(metadataDigestSchema),
216
+ dappMetadataDigest: normalizeBytes32Hex(metadataDigest),
217
+ functionRoot: normalizeBytes32Hex(functionRoot),
218
+ grothVerifier: getAddress(verifierSnapshot.grothVerifier),
219
+ grothVerifierCompatibleBackendVersion: requireVersionString(
220
+ verifierSnapshot.grothVerifierCompatibleBackendVersion,
221
+ "registered DApp Groth16 verifier compatibleBackendVersion",
222
+ ),
223
+ tokamakVerifier: getAddress(verifierSnapshot.tokamakVerifier),
224
+ tokamakVerifierCompatibleBackendVersion: requireVersionString(
225
+ verifierSnapshot.tokamakVerifierCompatibleBackendVersion,
226
+ "registered DApp Tokamak verifier compatibleBackendVersion",
227
+ ),
228
+ };
229
+ }
230
+
160
231
  async function prepareDeploymentArtifacts(chainId) {
161
232
  const normalizedChainId = Number(chainId);
162
233
  const existingPaths = flatDeploymentArtifactPathsByChainId.get(normalizedChainId);
@@ -364,16 +435,25 @@ async function handleChannelCreate({ args, network, provider }) {
364
435
  );
365
436
  const canonicalAsset = getAddress(await bridgeCore.canonicalAsset());
366
437
  const canonicalAssetDecimals = await fetchTokenDecimals(provider, canonicalAsset);
367
- const joinFeeInput = requireArg(args.joinFee, "--join-fee");
368
- const joinFee = parseTokenAmount(joinFeeInput, canonicalAssetDecimals);
438
+ const joinTollInput = requireArg(args.joinToll, "--join-toll");
439
+ const joinToll = parseTokenAmount(joinTollInput, canonicalAssetDecimals);
369
440
  const channelId = deriveChannelIdFromName(channelName);
370
- const dappId = await resolveDAppIdByLabel({
441
+ const dapp = await resolveDAppIdByLabel({
371
442
  provider,
372
443
  bridgeResources,
373
444
  dappLabel: PRIVATE_STATE_DAPP_LABEL,
374
445
  });
446
+ const dappId = dapp.dappId;
447
+ const policySnapshot = dapp.policySnapshot;
375
448
 
376
- const receipt = await waitForReceipt(await bridgeCore.createChannel(channelId, dappId, leader, joinFee));
449
+ printImmutableChannelPolicyWarning({
450
+ action: "create-channel",
451
+ channelName,
452
+ channelId,
453
+ policySnapshot,
454
+ });
455
+ const receipt =
456
+ await waitForReceipt(await bridgeCore.createChannel(channelId, dappId, joinToll, dapp.metadataDigest));
377
457
  const channelInfo = await bridgeCore.getChannel(channelId);
378
458
 
379
459
  const workspaceResult = await initializeChannelWorkspace({
@@ -390,9 +470,12 @@ async function handleChannelCreate({ args, network, provider }) {
390
470
  channelName,
391
471
  channelId: channelId.toString(),
392
472
  dappId,
473
+ dappMetadataDigest: dapp.metadataDigest,
474
+ dappMetadataDigestSchema: dapp.metadataDigestSchema,
475
+ policySnapshot,
393
476
  leader,
394
- joinFeeBaseUnits: joinFee.toString(),
395
- joinFeeTokens: ethers.formatUnits(joinFee, canonicalAssetDecimals),
477
+ joinTollBaseUnits: joinToll.toString(),
478
+ joinTollTokens: ethers.formatUnits(joinToll, canonicalAssetDecimals),
396
479
  canonicalAsset,
397
480
  canonicalAssetDecimals,
398
481
  asset: channelInfo.asset,
@@ -417,9 +500,21 @@ async function resolveDAppIdByLabel({ provider, bridgeResources, dappLabel }) {
417
500
  const manifestLabel = typeof manifest.dappLabel === "string" ? manifest.dappLabel : null;
418
501
  const manifestDappId = manifest.dappId;
419
502
  const manifestManager = typeof manifest.dAppManager === "string" ? getAddress(manifest.dAppManager) : null;
503
+ const manifestMetadataDigest = normalizeBytes32Hex(manifest.registration?.metadataDigest);
504
+ const manifestMetadataDigestSchema = normalizeBytes32Hex(manifest.registration?.metadataDigestSchema);
505
+ const manifestFunctionRoot = normalizeBytes32Hex(manifest.registration?.functionRoot);
420
506
 
421
507
  expect(manifestLabel === dappLabel, `DApp registration manifest label mismatch in ${manifestPath}.`);
422
508
  expect(Number.isInteger(manifestDappId), `DApp registration manifest is missing an integer dappId: ${manifestPath}.`);
509
+ expect(manifestMetadataDigest !== null, `DApp registration manifest is missing registration.metadataDigest: ${manifestPath}.`);
510
+ expect(
511
+ manifestMetadataDigestSchema !== null,
512
+ `DApp registration manifest is missing registration.metadataDigestSchema: ${manifestPath}.`,
513
+ );
514
+ expect(
515
+ manifestFunctionRoot !== null,
516
+ `DApp registration manifest is missing registration.functionRoot: ${manifestPath}.`,
517
+ );
423
518
  expect(
424
519
  manifestManager !== null
425
520
  && ethers.toBigInt(manifestManager) === ethers.toBigInt(getAddress(bridgeResources.bridgeDeployment.dAppManager)),
@@ -432,7 +527,36 @@ async function resolveDAppIdByLabel({ provider, bridgeResources, dappLabel }) {
432
527
  ethers.toBigInt(normalizeBytes32Hex(info.labelHash)) === ethers.toBigInt(expectedLabelHash),
433
528
  `DApp id ${manifestDappId} from ${manifestPath} does not match label ${dappLabel} on-chain.`,
434
529
  );
435
- return Number(manifestDappId);
530
+ const onchainMetadataDigest = normalizeBytes32Hex(info.metadataDigest);
531
+ const onchainMetadataDigestSchema = normalizeBytes32Hex(info.metadataDigestSchema);
532
+ const onchainFunctionRoot = normalizeBytes32Hex(info.functionRoot);
533
+ const verifierSnapshot = await dAppManager.getDAppVerifierSnapshot(manifestDappId);
534
+ const policySnapshot = normalizeDAppPolicySnapshot({
535
+ dappId: manifestDappId,
536
+ metadataDigest: onchainMetadataDigest,
537
+ metadataDigestSchema: onchainMetadataDigestSchema,
538
+ functionRoot: onchainFunctionRoot,
539
+ verifierSnapshot,
540
+ });
541
+ expect(
542
+ ethers.toBigInt(onchainMetadataDigest) === ethers.toBigInt(manifestMetadataDigest),
543
+ `DApp id ${manifestDappId} metadata digest ${onchainMetadataDigest} does not match ${manifestMetadataDigest} from ${manifestPath}.`,
544
+ );
545
+ expect(
546
+ ethers.toBigInt(onchainMetadataDigestSchema) === ethers.toBigInt(manifestMetadataDigestSchema),
547
+ `DApp id ${manifestDappId} metadata digest schema ${onchainMetadataDigestSchema} does not match ${manifestMetadataDigestSchema} from ${manifestPath}.`,
548
+ );
549
+ expect(
550
+ ethers.toBigInt(onchainFunctionRoot) === ethers.toBigInt(manifestFunctionRoot),
551
+ `DApp id ${manifestDappId} function root ${onchainFunctionRoot} does not match ${manifestFunctionRoot} from ${manifestPath}.`,
552
+ );
553
+ return {
554
+ dappId: Number(manifestDappId),
555
+ metadataDigest: onchainMetadataDigest,
556
+ metadataDigestSchema: onchainMetadataDigestSchema,
557
+ functionRoot: onchainFunctionRoot,
558
+ policySnapshot,
559
+ };
436
560
  }
437
561
 
438
562
  async function handleWorkspaceInit({ args, network, provider }) {
@@ -505,6 +629,10 @@ async function initializeChannelWorkspace({
505
629
  const currentRootVectorHash = normalizeBytes32Hex(await channelManager.currentRootVectorHash());
506
630
  const genesisBlockNumber = Number(await channelManager.genesisBlockNumber());
507
631
  const managedStorageAddresses = normalizedAddressVector(await channelManager.getManagedStorageAddresses());
632
+ const policySnapshot = await readChannelPolicySnapshot({
633
+ channelManager,
634
+ dappId: Number(channelInfo.dappId),
635
+ });
508
636
  const deploymentManifestPath = dappDeploymentManifestPath(network.chainId);
509
637
  const storageLayoutManifestPath = dappStorageLayoutManifestPath(network.chainId);
510
638
  const deploymentManifest = readJson(deploymentManifestPath);
@@ -571,6 +699,10 @@ async function initializeChannelWorkspace({
571
699
  controller: controllerAddress,
572
700
  l2AccountingVault: l2AccountingVaultAddress,
573
701
  aPubBlockHash: normalizeBytes32Hex(channelInfo.aPubBlockHash),
702
+ dappMetadataDigestSchema: policySnapshot.dappMetadataDigestSchema,
703
+ dappMetadataDigest: policySnapshot.dappMetadataDigest,
704
+ functionRoot: policySnapshot.functionRoot,
705
+ policySnapshot,
574
706
  managedStorageAddresses,
575
707
  liquidBalancesSlot: liquidBalancesSlot.toString(),
576
708
  };
@@ -967,7 +1099,7 @@ async function handleInstallZkEvm({ args }) {
967
1099
  const deploymentArtifacts = await installPrivateStateCliArtifacts({
968
1100
  dappName: PRIVATE_STATE_DAPP_LABEL,
969
1101
  localDeploymentBaseRoot,
970
- groth16CrsVersion: selectedVersions.groth16,
1102
+ groth16CrsVersion: groth16Runtime.compatibleBackendVersion,
971
1103
  });
972
1104
  const installManifest = writePrivateStateCliInstallManifest({
973
1105
  dockerRequested: Boolean(args.docker),
@@ -1185,20 +1317,27 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
1185
1317
  let resolvedLeafIndex = leafIndex;
1186
1318
  let approveReceipt = null;
1187
1319
  let receipt = null;
1188
- let joinFee = 0n;
1320
+ let joinToll = 0n;
1189
1321
  let status = null;
1190
1322
 
1191
1323
  if (!existingRegistration.exists) {
1192
- joinFee = ethers.toBigInt(await context.channelManager.joinFee());
1324
+ joinToll = ethers.toBigInt(await context.channelManager.joinToll());
1193
1325
  const asset = new Contract(
1194
1326
  context.workspace.canonicalAsset,
1195
1327
  context.bridgeAbiManifest.contracts.erc20.abi,
1196
1328
  signer,
1197
1329
  );
1198
1330
  let nextNonce = await provider.getTransactionCount(signer.address, "pending");
1199
- if (joinFee !== 0n) {
1331
+ printImmutableChannelPolicyWarning({
1332
+ action: "join-channel",
1333
+ channelName: context.workspace.channelName,
1334
+ channelId: ethers.toBigInt(context.workspace.channelId),
1335
+ channelManager: context.workspace.channelManager,
1336
+ policySnapshot: context.workspace.policySnapshot,
1337
+ });
1338
+ if (joinToll !== 0n) {
1200
1339
  approveReceipt = await waitForReceipt(
1201
- await asset.approve(context.workspace.bridgeTokenVault, joinFee, { nonce: nextNonce++ }),
1340
+ await asset.approve(context.workspace.bridgeTokenVault, joinToll, { nonce: nextNonce++ }),
1202
1341
  );
1203
1342
  }
1204
1343
  receipt = await waitForReceipt(
@@ -1232,7 +1371,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
1232
1371
  "The existing note-receive public key parity does not match the derived note-receive public key.",
1233
1372
  );
1234
1373
  resolvedLeafIndex = existingRegistration.leafIndex;
1235
- joinFee = ethers.toBigInt(existingRegistration.joinFeePaid);
1374
+ joinToll = ethers.toBigInt(existingRegistration.joinTollPaid);
1236
1375
  status = "already-registered";
1237
1376
  }
1238
1377
 
@@ -1258,9 +1397,10 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
1258
1397
  l2Address: l2Identity.l2Address,
1259
1398
  l2StorageKey: storageKey,
1260
1399
  leafIndex: resolvedLeafIndex.toString(),
1261
- joinFeeBaseUnits: joinFee.toString(),
1262
- joinFeeTokens: ethers.formatUnits(joinFee, Number(context.workspace.canonicalAssetDecimals)),
1400
+ joinTollBaseUnits: joinToll.toString(),
1401
+ joinTollTokens: ethers.formatUnits(joinToll, Number(context.workspace.canonicalAssetDecimals)),
1263
1402
  noteReceivePubKey: noteReceiveKeyMaterial.noteReceivePubKey,
1403
+ policySnapshot: context.workspace.policySnapshot,
1264
1404
  approveGasUsed: approveReceipt ? receiptGasUsed(approveReceipt) : null,
1265
1405
  gasUsed: receipt ? receiptGasUsed(receipt) : null,
1266
1406
  approveTxUrl: approveReceipt ? explorerTxUrl(network, approveReceipt.hash) : null,
@@ -1287,7 +1427,7 @@ async function handleExitChannel({ args, provider }) {
1287
1427
  "Run withdraw-channel first, or rerun exit-channel with --force to bypass this CLI check.",
1288
1428
  ].join(" "),
1289
1429
  );
1290
- const [refundAmount, refundBps] = await context.channelManager.getExitFeeRefundQuote(signer.address);
1430
+ const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(signer.address);
1291
1431
  const receipt = await waitForReceipt(
1292
1432
  await context.bridgeTokenVault.connect(signer).exitChannel(ethers.toBigInt(context.workspace.channelId)),
1293
1433
  );
@@ -1393,8 +1533,9 @@ async function handleGrothVaultMove({ args, provider, direction }) {
1393
1533
  nextValue,
1394
1534
  });
1395
1535
 
1536
+ const methodName = direction === "deposit" ? "depositToChannelVault" : "withdrawFromChannelVault";
1396
1537
  const receipt = await waitForReceipt(
1397
- await bridgeTokenVault[direction](ethers.toBigInt(context.workspace.channelId), transition.proof, transition.update),
1538
+ await bridgeTokenVault[methodName](ethers.toBigInt(context.workspace.channelId), transition.proof, transition.update),
1398
1539
  );
1399
1540
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
1400
1541
  expect(
@@ -1453,6 +1594,63 @@ async function handleWithdrawBridge({ args, network, provider }) {
1453
1594
  });
1454
1595
  }
1455
1596
 
1597
+ function resolveFunctionMetadataProofForExecution({
1598
+ chainId,
1599
+ controllerAddress,
1600
+ functionSelector,
1601
+ preprocessInputHash,
1602
+ expectedFunctionRoot,
1603
+ }) {
1604
+ const manifestPath = requireFlatDeploymentArtifactPathsForChainId(chainId).dappRegistrationPath;
1605
+ const manifest = readJson(manifestPath);
1606
+ const proofRoot = normalizeBytes32Hex(manifest.functionMetadataProofs?.root);
1607
+ const expectedRoot = normalizeBytes32Hex(expectedFunctionRoot);
1608
+ expect(
1609
+ ethers.toBigInt(proofRoot) === ethers.toBigInt(expectedRoot),
1610
+ `DApp function proof root ${proofRoot} does not match channel function root ${expectedRoot}.`,
1611
+ );
1612
+
1613
+ const functions = manifest.functionMetadataProofs?.functions;
1614
+ expect(Array.isArray(functions), `DApp registration manifest is missing functionMetadataProofs.functions: ${manifestPath}.`);
1615
+ const normalizedController = getAddress(controllerAddress);
1616
+ const normalizedSelector = normalizeBytesHex(functionSelector, 4);
1617
+ const normalizedPreprocessInputHash = normalizeBytes32Hex(preprocessInputHash);
1618
+ const entry = functions.find((candidate) => {
1619
+ const metadata = candidate?.metadata;
1620
+ return metadata
1621
+ && getAddress(metadata.entryContract) === normalizedController
1622
+ && normalizeBytesHex(metadata.functionSig, 4) === normalizedSelector
1623
+ && normalizeBytes32Hex(metadata.preprocessInputHash) === normalizedPreprocessInputHash;
1624
+ });
1625
+ expect(
1626
+ entry !== undefined,
1627
+ [
1628
+ `No DApp function metadata proof found for ${normalizedController} ${normalizedSelector}.`,
1629
+ `Expected preprocess input hash: ${normalizedPreprocessInputHash}.`,
1630
+ `Manifest: ${manifestPath}.`,
1631
+ ].join(" "),
1632
+ );
1633
+
1634
+ return {
1635
+ metadata: {
1636
+ entryContract: getAddress(entry.metadata.entryContract),
1637
+ functionSig: normalizeBytesHex(entry.metadata.functionSig, 4),
1638
+ preprocessInputHash: normalizeBytes32Hex(entry.metadata.preprocessInputHash),
1639
+ instanceLayout: {
1640
+ entryContractOffsetWords: Number(entry.metadata.instanceLayout.entryContractOffsetWords),
1641
+ functionSigOffsetWords: Number(entry.metadata.instanceLayout.functionSigOffsetWords),
1642
+ currentRootVectorOffsetWords: Number(entry.metadata.instanceLayout.currentRootVectorOffsetWords),
1643
+ updatedRootVectorOffsetWords: Number(entry.metadata.instanceLayout.updatedRootVectorOffsetWords),
1644
+ eventLogs: entry.metadata.instanceLayout.eventLogs.map((eventLog) => ({
1645
+ startOffsetWords: Number(eventLog.startOffsetWords),
1646
+ topicCount: Number(eventLog.topicCount),
1647
+ })),
1648
+ },
1649
+ },
1650
+ siblings: entry.merkleProof.map((sibling) => normalizeBytes32Hex(sibling)),
1651
+ };
1652
+ }
1653
+
1456
1654
  async function handleMintNotes({ args, provider }) {
1457
1655
  const { wallet } = loadUnlockedWalletWithMetadata(args);
1458
1656
  const canonicalAssetDecimals = Number(wallet.wallet.canonicalAssetDecimals);
@@ -3109,6 +3307,16 @@ async function executeWalletTemplateSend({
3109
3307
  writeJson(path.join(operationDir, "resource", "synthesizer", "output", "state_snapshot.normalized.json"), nextSnapshot);
3110
3308
 
3111
3309
  const payload = loadTokamakPayloadFromStep(operationDir);
3310
+ const functionProof = resolveFunctionMetadataProofForExecution({
3311
+ chainId: context.workspace.chainId,
3312
+ controllerAddress: context.workspace.controller,
3313
+ functionSelector: calldata.slice(0, 10),
3314
+ preprocessInputHash: hashTokamakPointEncoding(
3315
+ payload.functionPreprocessPart1,
3316
+ payload.functionPreprocessPart2,
3317
+ ),
3318
+ expectedFunctionRoot: context.workspace.functionRoot ?? context.workspace.policySnapshot?.functionRoot,
3319
+ });
3112
3320
  const noteLifecycle = extractControllerStorageDelta({
3113
3321
  previousSnapshot: context.currentSnapshot,
3114
3322
  nextSnapshot,
@@ -3123,7 +3331,9 @@ async function executeWalletTemplateSend({
3123
3331
  );
3124
3332
 
3125
3333
  const receipt =
3126
- await waitForReceipt(await context.channelManager.connect(signer).executeChannelTransaction(payload));
3334
+ await waitForReceipt(
3335
+ await context.channelManager.connect(signer).executeChannelTransaction(payload, functionProof),
3336
+ );
3127
3337
 
3128
3338
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
3129
3339
  expect(
@@ -3445,14 +3655,22 @@ async function assertChannelProofBackendVersionCompatibility({ context, operatio
3445
3655
  {
3446
3656
  label: "Groth16",
3447
3657
  packageName: GROTH16_PACKAGE_NAME,
3448
- channelVersion: channelVersions.groth16,
3449
- localVersion: localVersions.groth16,
3658
+ versionKind: "compatible backend version",
3659
+ channelVersion: requireCanonicalGroth16CompatibleBackendVersion(
3660
+ channelVersions.groth16,
3661
+ "channel Groth16 verifier compatibleBackendVersion",
3662
+ ),
3663
+ localVersion: localVersions.groth16.compatibleBackendVersion,
3450
3664
  },
3451
3665
  {
3452
3666
  label: "Tokamak zk-EVM",
3453
3667
  packageName: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
3454
- channelVersion: channelVersions.tokamak,
3455
- localVersion: localVersions.tokamak,
3668
+ versionKind: "compatible backend version",
3669
+ channelVersion: requireCanonicalCompatibleBackendVersion(
3670
+ channelVersions.tokamak,
3671
+ "channel Tokamak verifier compatibleBackendVersion",
3672
+ ),
3673
+ localVersion: localVersions.tokamak.compatibleBackendVersion,
3456
3674
  },
3457
3675
  ];
3458
3676
  const mismatches = checks.filter(({ channelVersion, localVersion }) => channelVersion !== localVersion);
@@ -3464,10 +3682,11 @@ async function assertChannelProofBackendVersionCompatibility({ context, operatio
3464
3682
  [
3465
3683
  `Channel proof backend version mismatch before ${operationName} proof generation.`,
3466
3684
  `Channel: ${context.workspace.channelName ?? context.workspaceName ?? context.workspace.channelId}.`,
3467
- ...mismatches.map(({ label, packageName, channelVersion, localVersion }) => (
3468
- `${label} verifier expects ${packageName} ${channelVersion}, but the local installed package version is ${localVersion}.`
3685
+ ...mismatches.map(({ label, packageName, versionKind, channelVersion, localVersion }) => (
3686
+ `${label} verifier expects ${packageName} ${versionKind} ${channelVersion}, `
3687
+ + `but the local installed ${versionKind} is ${localVersion ?? "<missing>"}.`
3469
3688
  )),
3470
- "Install the matching CLI package versions before generating proofs for this channel.",
3689
+ "Install proof backend runtimes compatible with this channel before generating proofs.",
3471
3690
  ].join(" "),
3472
3691
  );
3473
3692
  }
@@ -3499,29 +3718,92 @@ async function readChannelVerifierCompatibleBackendVersions(context) {
3499
3718
  }
3500
3719
  }
3501
3720
 
3721
+ async function readChannelPolicySnapshot({ channelManager, dappId }) {
3722
+ const channelManagerAddress = getAddress(await channelManager.getAddress());
3723
+ try {
3724
+ const [
3725
+ dappMetadataDigestSchema,
3726
+ dappMetadataDigest,
3727
+ functionRoot,
3728
+ grothVerifier,
3729
+ grothVerifierCompatibleBackendVersion,
3730
+ tokamakVerifier,
3731
+ tokamakVerifierCompatibleBackendVersion,
3732
+ ] = await Promise.all([
3733
+ channelManager.dappMetadataDigestSchema(),
3734
+ channelManager.dappMetadataDigest(),
3735
+ channelManager.functionRoot(),
3736
+ channelManager.grothVerifier(),
3737
+ channelManager.grothVerifierCompatibleBackendVersion(),
3738
+ channelManager.tokamakVerifier(),
3739
+ channelManager.tokamakVerifierCompatibleBackendVersion(),
3740
+ ]);
3741
+ return {
3742
+ dappId: Number(dappId),
3743
+ dappMetadataDigestSchema: normalizeBytes32Hex(dappMetadataDigestSchema),
3744
+ dappMetadataDigest: normalizeBytes32Hex(dappMetadataDigest),
3745
+ functionRoot: normalizeBytes32Hex(functionRoot),
3746
+ grothVerifier: getAddress(grothVerifier),
3747
+ grothVerifierCompatibleBackendVersion: requireVersionString(
3748
+ grothVerifierCompatibleBackendVersion,
3749
+ "channel Groth16 verifier compatibleBackendVersion",
3750
+ ),
3751
+ tokamakVerifier: getAddress(tokamakVerifier),
3752
+ tokamakVerifierCompatibleBackendVersion: requireVersionString(
3753
+ tokamakVerifierCompatibleBackendVersion,
3754
+ "channel Tokamak verifier compatibleBackendVersion",
3755
+ ),
3756
+ };
3757
+ } catch (error) {
3758
+ throw new Error(
3759
+ [
3760
+ `Unable to read immutable policy snapshot from channel manager ${channelManagerAddress}.`,
3761
+ "The target channel must expose DApp digest, verifier address, and compatibleBackendVersion getters.",
3762
+ ].join(" "),
3763
+ { cause: error },
3764
+ );
3765
+ }
3766
+ }
3767
+
3502
3768
  function readLocalProofBackendPackageVersions() {
3769
+ const groth16Runtime = inspectGroth16Runtime();
3770
+ const tokamakPackageReport = readTokamakCliPackageReport();
3503
3771
  return {
3504
- groth16: requirePackageReportVersion(
3505
- readPackageReport({
3506
- name: GROTH16_PACKAGE_NAME,
3507
- packageJsonPath: path.join(resolveActiveGroth16PackageRoot(), "package.json"),
3508
- }),
3509
- ),
3510
- tokamak: requirePackageReportVersion(readTokamakCliPackageReport()),
3772
+ groth16: {
3773
+ packageVersion: groth16Runtime.packageVersion,
3774
+ compatibleBackendVersion: groth16Runtime.crsCompatibleBackendVersion
3775
+ ?? groth16Runtime.compatibleBackendVersion,
3776
+ },
3777
+ tokamak: {
3778
+ packageVersion: requirePackageReportVersion(tokamakPackageReport),
3779
+ compatibleBackendVersion: requirePackageReportCompatibleBackendVersion(tokamakPackageReport),
3780
+ },
3511
3781
  };
3512
3782
  }
3513
3783
 
3514
- function readTokamakCliPackageReport() {
3784
+ function readTokamakCliPackageReport(packageRoot = null) {
3515
3785
  try {
3516
- return readPackageReport({
3786
+ const resolvedPackageRoot = packageRoot ?? resolveActiveTokamakCliPackageRoot();
3787
+ const packageJsonPath = path.join(resolvedPackageRoot, "package.json");
3788
+ const packageJson = readJson(packageJsonPath);
3789
+ const report = readPackageReport({
3517
3790
  name: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
3518
- packageJsonPath: path.join(resolveActiveTokamakCliPackageRoot(), "package.json"),
3791
+ packageJsonPath,
3792
+ packageJson,
3519
3793
  });
3794
+ return {
3795
+ ...report,
3796
+ compatibleBackendVersion: readTokamakZkEvmCompatibleBackendVersionFromPackageJson(
3797
+ packageJson,
3798
+ TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
3799
+ ),
3800
+ };
3520
3801
  } catch (error) {
3521
3802
  return {
3522
3803
  name: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
3523
3804
  version: null,
3524
3805
  packageRoot: null,
3806
+ compatibleBackendVersion: null,
3525
3807
  error: error.message,
3526
3808
  ok: false,
3527
3809
  };
@@ -3537,6 +3819,15 @@ function requirePackageReportVersion(report) {
3537
3819
  return requireVersionString(report.version, `${report.name} package version`);
3538
3820
  }
3539
3821
 
3822
+ function requirePackageReportCompatibleBackendVersion(report) {
3823
+ if (!report.compatibleBackendVersion) {
3824
+ throw new Error(
3825
+ `Unable to determine local ${report.name} compatible backend version${report.error ? `: ${report.error}` : "."}`,
3826
+ );
3827
+ }
3828
+ return requireVersionString(report.compatibleBackendVersion, `${report.name} compatible backend version`);
3829
+ }
3830
+
3540
3831
  function requireVersionString(value, label) {
3541
3832
  const normalized = String(value ?? "").trim();
3542
3833
  expect(normalized.length > 0, `${label} is missing.`);
@@ -3864,10 +4155,6 @@ function normalizeBytes16Hex(value) {
3864
4155
  return normalizeBytesHex(value, 16);
3865
4156
  }
3866
4157
 
3867
- function normalizeBytes20Hex(value) {
3868
- return normalizeBytesHex(value, 20);
3869
- }
3870
-
3871
4158
  function normalizeBytes32Hex(hexValue) {
3872
4159
  return normalizeBytesHex(hexValue, 32);
3873
4160
  }
@@ -3888,6 +4175,10 @@ function hashTokamakPublicInputs(values) {
3888
4175
  return keccak256(abiCoder.encode(["uint256[]"], [values]));
3889
4176
  }
3890
4177
 
4178
+ function hashTokamakPointEncoding(part1, part2) {
4179
+ return keccak256(abiCoder.encode(["uint128[]", "uint256[]"], [part1, part2]));
4180
+ }
4181
+
3891
4182
  function encodeTokamakBlockInfo(blockInfo) {
3892
4183
  const values = new Array(TOKAMAK_APUB_BLOCK_LENGTH).fill(0n);
3893
4184
  appendSplitWord(values, 0, ethers.toBigInt(blockInfo.coinBase));
@@ -4774,15 +5065,15 @@ function assertGetMyNotesArgs(args) {
4774
5065
 
4775
5066
  function assertCreateChannelArgs(args) {
4776
5067
  requireArg(args.channelName, "--channel-name");
4777
- requireArg(args.joinFee, "--join-fee");
5068
+ requireArg(args.joinToll, "--join-toll");
4778
5069
  requireNetworkName(args);
4779
5070
  requireAlchemyApiKeyForPublicNetwork(args, "create-channel");
4780
5071
  requireArg(args.privateKey, "--private-key");
4781
5072
  assertAllowedCommandKeys(
4782
5073
  args,
4783
5074
  "create-channel",
4784
- new Set(["command", "positional", "channelName", "joinFee", "network", "alchemyApiKey", "privateKey"]),
4785
- "--channel-name, --join-fee, --network, --private-key, and --alchemy-api-key on public networks",
5075
+ new Set(["command", "positional", "channelName", "joinToll", "network", "alchemyApiKey", "privateKey"]),
5076
+ "--channel-name, --join-toll, --network, --private-key, and --alchemy-api-key on public networks",
4786
5077
  );
4787
5078
  }
4788
5079
 
@@ -4944,8 +5235,9 @@ Commands:
4944
5235
  --doctor
4945
5236
  Check private-state CLI package versions, runtime install state, Docker mode, CUDA mode, and deployment artifacts
4946
5237
 
4947
- create-channel --channel-name <NAME> --join-fee <TOKENS> --network <NAME> --private-key <HEX> --alchemy-api-key <KEY>
5238
+ create-channel --channel-name <NAME> --join-toll <TOKENS> --network <NAME> --private-key <HEX> --alchemy-api-key <KEY>
4948
5239
  Create a bridge channel and initialize its workspace
5240
+ Prints the immutable policy snapshot before sending the transaction
4949
5241
 
4950
5242
  recover-workspace --channel-name <NAME> --network <NAME> --alchemy-api-key <KEY>
4951
5243
  Rebuild the local channel workspace from bridge state
@@ -4963,7 +5255,8 @@ Commands:
4963
5255
  Rebuild a recoverable local wallet from on-chain channel state
4964
5256
 
4965
5257
  join-channel --channel-name <NAME> --password <PASSWORD> --network <NAME> --private-key <HEX> --alchemy-api-key <KEY>
4966
- Pay the channel join fee and bind a wallet to a channel-specific L2 identity
5258
+ Pay the channel join toll and bind a wallet to a channel-specific L2 identity
5259
+ Prints the immutable policy snapshot before first registration
4967
5260
 
4968
5261
  get-my-wallet-meta --wallet <NAME> --password <PASSWORD> --network <NAME>
4969
5262
  Check whether a wallet matches the on-chain channel registration
@@ -5198,11 +5491,7 @@ function expect(condition, message) {
5198
5491
  }
5199
5492
 
5200
5493
  function requireSemverVersion(value, label) {
5201
- const normalized = requireNonEmptyString(value, label);
5202
- if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?(?:\+[0-9A-Za-z.]+)?$/.test(normalized)) {
5203
- throw new Error(`${label} must be an exact semantic version. Received: ${normalized}`);
5204
- }
5205
- return normalized;
5494
+ return requireExactSemverVersion(value, label);
5206
5495
  }
5207
5496
 
5208
5497
  function resolveArtifactCacheBaseRoot(
@@ -5386,22 +5675,41 @@ function buildDoctorReport() {
5386
5675
 
5387
5676
  function buildSelectedRuntimeVersionCheck({ installManifest, tokamakCli, groth16Runtime }) {
5388
5677
  const selectedVersions = installManifest?.install?.selectedVersions ?? null;
5678
+ const selectedTokamakCompatibleBackendVersion = selectedVersions?.tokamak
5679
+ ? normalizePackageVersionToCompatibleBackendVersion(
5680
+ selectedVersions.tokamak,
5681
+ "selected Tokamak zk-EVM CLI version",
5682
+ )
5683
+ : null;
5684
+ const selectedGroth16CompatibleBackendVersion = selectedVersions?.groth16
5685
+ ? normalizePackageVersionToCompatibleBackendVersion(selectedVersions.groth16, "selected Groth16 CLI version")
5686
+ : null;
5389
5687
  const details = [
5390
5688
  {
5391
5689
  name: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
5392
5690
  selectedVersion: selectedVersions?.tokamak ?? null,
5691
+ selectedCompatibleBackendVersion: selectedTokamakCompatibleBackendVersion,
5393
5692
  installedVersion: tokamakCli.packageVersion ?? null,
5394
- ok: !selectedVersions?.tokamak || selectedVersions.tokamak === tokamakCli.packageVersion,
5693
+ compatibleBackendVersion: tokamakCli.compatibleBackendVersion ?? null,
5694
+ ok: !selectedVersions?.tokamak
5695
+ || (
5696
+ selectedVersions.tokamak === tokamakCli.packageVersion
5697
+ && selectedTokamakCompatibleBackendVersion === tokamakCli.compatibleBackendVersion
5698
+ ),
5395
5699
  },
5396
5700
  {
5397
5701
  name: GROTH16_PACKAGE_NAME,
5398
5702
  selectedVersion: selectedVersions?.groth16 ?? null,
5703
+ selectedCompatibleBackendVersion: selectedGroth16CompatibleBackendVersion,
5399
5704
  installedVersion: groth16Runtime.packageVersion ?? null,
5705
+ compatibleBackendVersion: groth16Runtime.compatibleBackendVersion ?? null,
5400
5706
  crsVersion: groth16Runtime.crsVersion ?? null,
5707
+ crsCompatibleBackendVersion: groth16Runtime.crsCompatibleBackendVersion ?? null,
5401
5708
  ok: !selectedVersions?.groth16
5402
5709
  || (
5403
5710
  selectedVersions.groth16 === groth16Runtime.packageVersion
5404
- && selectedVersions.groth16 === groth16Runtime.crsVersion
5711
+ && selectedGroth16CompatibleBackendVersion === groth16Runtime.compatibleBackendVersion
5712
+ && selectedGroth16CompatibleBackendVersion === groth16Runtime.crsCompatibleBackendVersion
5405
5713
  ),
5406
5714
  },
5407
5715
  ];
@@ -5414,11 +5722,7 @@ function buildSelectedRuntimeVersionCheck({ installManifest, tokamakCli, groth16
5414
5722
 
5415
5723
  async function resolvePrivateStateInstallRuntimeVersions(args) {
5416
5724
  const [groth16, tokamak] = await Promise.all([
5417
- resolveRequestedNpmPackageVersion({
5418
- packageName: GROTH16_PACKAGE_NAME,
5419
- requestedVersion: args.groth16CliVersion,
5420
- optionName: "--groth16-cli-version",
5421
- }),
5725
+ resolveRequestedGroth16PackageVersion(args.groth16CliVersion),
5422
5726
  resolveRequestedNpmPackageVersion({
5423
5727
  packageName: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
5424
5728
  requestedVersion: args.tokamakZkEvmCliVersion,
@@ -5428,6 +5732,19 @@ async function resolvePrivateStateInstallRuntimeVersions(args) {
5428
5732
  return { groth16, tokamak };
5429
5733
  }
5430
5734
 
5735
+ async function resolveRequestedGroth16PackageVersion(requestedVersion) {
5736
+ if (requestedVersion !== undefined && requestedVersion !== null) {
5737
+ return resolveRequestedNpmPackageVersion({
5738
+ packageName: GROTH16_PACKAGE_NAME,
5739
+ requestedVersion,
5740
+ optionName: "--groth16-cli-version",
5741
+ });
5742
+ }
5743
+
5744
+ const bundledPackageJson = readJson(path.join(resolveGroth16PackageRoot(), "package.json"));
5745
+ return requireSemverVersion(bundledPackageJson.version, `${GROTH16_PACKAGE_NAME} bundled package version`);
5746
+ }
5747
+
5431
5748
  async function resolveRequestedNpmPackageVersion({ packageName, requestedVersion, optionName }) {
5432
5749
  const metadata = await fetchNpmPackageMetadata(packageName);
5433
5750
  if (requestedVersion === undefined || requestedVersion === null) {
@@ -5441,20 +5758,6 @@ async function resolveRequestedNpmPackageVersion({ packageName, requestedVersion
5441
5758
  return normalizedVersion;
5442
5759
  }
5443
5760
 
5444
- async function fetchNpmPackageMetadata(packageName) {
5445
- const normalizedPackageName = requireNonEmptyString(packageName, "packageName");
5446
- const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(normalizedPackageName)}`;
5447
- const response = await fetch(registryUrl, { redirect: "follow" });
5448
- if (!response.ok) {
5449
- throw new Error(`Failed to read npm package metadata for ${normalizedPackageName}: HTTP ${response.status}.`);
5450
- }
5451
- try {
5452
- return await response.json();
5453
- } catch (error) {
5454
- throw new Error(`npm package metadata for ${normalizedPackageName} is not valid JSON: ${error.message}`);
5455
- }
5456
- }
5457
-
5458
5761
  async function installTokamakCliRuntimeForPrivateState({ version, docker }) {
5459
5762
  const packageInstall = installManagedNpmPackage({
5460
5763
  packageName: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
@@ -5471,6 +5774,7 @@ async function installTokamakCliRuntimeForPrivateState({ version, docker }) {
5471
5774
  });
5472
5775
  const doctorOutput = stripAnsi(`${doctor.stdout}${doctor.stderr}`);
5473
5776
  const runtimeRoot = parseRuntimeRootFromTokamakDoctorOutput(doctorOutput);
5777
+ const compatibleBackendVersion = readTokamakCliPackageCompatibleBackendVersion(packageInstall.packageRoot);
5474
5778
  expect(
5475
5779
  doctor.status === 0 && runtimeRoot,
5476
5780
  [
@@ -5481,6 +5785,7 @@ async function installTokamakCliRuntimeForPrivateState({ version, docker }) {
5481
5785
  return {
5482
5786
  packageName: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
5483
5787
  packageVersion: version,
5788
+ compatibleBackendVersion,
5484
5789
  packageRoot: packageInstall.packageRoot,
5485
5790
  entryPath: invocation.entryPath,
5486
5791
  installPrefix: packageInstall.installPrefix,
@@ -5490,10 +5795,7 @@ async function installTokamakCliRuntimeForPrivateState({ version, docker }) {
5490
5795
  }
5491
5796
 
5492
5797
  async function installGroth16RuntimeForPrivateState({ version, docker }) {
5493
- const packageInstall = installManagedNpmPackage({
5494
- packageName: GROTH16_PACKAGE_NAME,
5495
- version,
5496
- });
5798
+ const packageInstall = resolveGroth16RuntimePackageInstall(version);
5497
5799
  const packageRoot = packageInstall.packageRoot;
5498
5800
  const entryPath = resolveGroth16CliEntryPath(packageRoot);
5499
5801
  const args = [entryPath, "--install", "--no-setup"];
@@ -5501,13 +5803,15 @@ async function installGroth16RuntimeForPrivateState({ version, docker }) {
5501
5803
  args.push("--docker");
5502
5804
  }
5503
5805
  run(process.execPath, args, { cwd: packageRoot });
5504
- const crsInstall = await installGroth16CrsForPrivateStateVersion(version);
5806
+ const compatibleBackendVersion = readGroth16PackageCompatibleBackendVersion(packageRoot);
5807
+ const crsInstall = await installGroth16CrsForPrivateStateVersion(compatibleBackendVersion);
5505
5808
  const runtime = inspectGroth16Runtime({ packageRoot });
5506
5809
  expect(runtime.installed, "Groth16 runtime install completed, but tokamak-groth16 --doctor still reports an unhealthy runtime.");
5507
5810
  return {
5508
5811
  ...runtime,
5509
5812
  packageName: GROTH16_PACKAGE_NAME,
5510
5813
  packageVersion: version,
5814
+ compatibleBackendVersion,
5511
5815
  packageRoot,
5512
5816
  entryPath,
5513
5817
  installPrefix: packageInstall.installPrefix,
@@ -5517,9 +5821,32 @@ async function installGroth16RuntimeForPrivateState({ version, docker }) {
5517
5821
  };
5518
5822
  }
5519
5823
 
5824
+ function resolveGroth16RuntimePackageInstall(version) {
5825
+ const normalizedVersion = requireSemverVersion(version, `${GROTH16_PACKAGE_NAME} version`);
5826
+ const bundledPackageRoot = resolveGroth16PackageRoot();
5827
+ const bundledPackageJson = readJson(path.join(bundledPackageRoot, "package.json"));
5828
+ if (bundledPackageJson.name === GROTH16_PACKAGE_NAME && bundledPackageJson.version === normalizedVersion) {
5829
+ return {
5830
+ packageName: GROTH16_PACKAGE_NAME,
5831
+ version: normalizedVersion,
5832
+ installPrefix: null,
5833
+ packageRoot: bundledPackageRoot,
5834
+ };
5835
+ }
5836
+
5837
+ return installManagedNpmPackage({
5838
+ packageName: GROTH16_PACKAGE_NAME,
5839
+ version: normalizedVersion,
5840
+ });
5841
+ }
5842
+
5520
5843
  async function installGroth16CrsForPrivateStateVersion(version) {
5521
5844
  const workspaceRoot = defaultGroth16WorkspaceRoot();
5522
5845
  const crsDir = path.join(workspaceRoot, "crs");
5846
+ const existingInstall = readExistingGroth16CrsInstall({ version, crsDir });
5847
+ if (existingInstall) {
5848
+ return existingInstall;
5849
+ }
5523
5850
  const crsInstall = await downloadPublicGroth16MpcArtifactsByVersion({
5524
5851
  version,
5525
5852
  outputDir: crsDir,
@@ -5541,6 +5868,56 @@ async function installGroth16CrsForPrivateStateVersion(version) {
5541
5868
  return crsInstall;
5542
5869
  }
5543
5870
 
5871
+ function readExistingGroth16CrsInstall({ version, crsDir }) {
5872
+ const normalizedVersion = requireCanonicalGroth16CompatibleBackendVersion(version, "Groth16 MPC CRS version");
5873
+ const selectedFiles = [
5874
+ "circuit_final.zkey",
5875
+ "verification_key.json",
5876
+ "metadata.json",
5877
+ "zkey_provenance.json",
5878
+ ];
5879
+ const targetPaths = selectedFiles.map((fileName) => path.join(crsDir, fileName));
5880
+ if (!targetPaths.every((targetPath) => fs.existsSync(targetPath))) {
5881
+ return null;
5882
+ }
5883
+ const metadata = readJson(path.join(crsDir, "metadata.json"));
5884
+ let metadataVersion;
5885
+ try {
5886
+ metadataVersion = requireCanonicalGroth16CompatibleBackendVersion(
5887
+ metadata.compatibleBackendVersion,
5888
+ "installed Groth16 MPC CRS version",
5889
+ );
5890
+ } catch {
5891
+ return null;
5892
+ }
5893
+ if (metadataVersion !== normalizedVersion) {
5894
+ return null;
5895
+ }
5896
+ const provenance = readJson(path.join(crsDir, "zkey_provenance.json"));
5897
+ return {
5898
+ source: "local-cache",
5899
+ archiveName: provenance.published_archive_name ?? null,
5900
+ archiveFileId: parseDriveFileIdFromDownloadUrl(provenance.zkey_download_url),
5901
+ folderUrl: provenance.published_folder_url ?? null,
5902
+ version: normalizedVersion,
5903
+ installedFiles: selectedFiles.map((archivePath, index) => ({
5904
+ archivePath,
5905
+ targetPath: targetPaths[index],
5906
+ })),
5907
+ };
5908
+ }
5909
+
5910
+ function parseDriveFileIdFromDownloadUrl(value) {
5911
+ if (typeof value !== "string" || value.length === 0) {
5912
+ return null;
5913
+ }
5914
+ try {
5915
+ return new URL(value).searchParams.get("id");
5916
+ } catch {
5917
+ return null;
5918
+ }
5919
+ }
5920
+
5544
5921
  function installManagedNpmPackage({ packageName, version, cacheBaseRoot = resolveArtifactCacheBaseRoot() }) {
5545
5922
  const normalizedPackageName = requireNonEmptyString(packageName, "packageName");
5546
5923
  const normalizedVersion = requireSemverVersion(version, `${normalizedPackageName} version`);
@@ -5625,15 +6002,15 @@ function collectDependencyPackageReports(installManifest = null) {
5625
6002
  });
5626
6003
  }
5627
6004
 
5628
- function readPackageReport({ name, packageJsonPath = null, resolveTarget = null }) {
6005
+ function readPackageReport({ name, packageJsonPath = null, packageJson = null, resolveTarget = null }) {
5629
6006
  try {
5630
6007
  const resolvedPackageJsonPath = packageJsonPath
5631
6008
  ? path.resolve(packageJsonPath)
5632
6009
  : findPackageJsonForName(path.dirname(require.resolve(resolveTarget ?? name)), name);
5633
- const packageJson = readJson(resolvedPackageJsonPath);
6010
+ const resolvedPackageJson = packageJson ?? readJson(resolvedPackageJsonPath);
5634
6011
  return {
5635
- name: packageJson.name ?? name,
5636
- version: packageJson.version ?? null,
6012
+ name: resolvedPackageJson.name ?? name,
6013
+ version: resolvedPackageJson.version ?? null,
5637
6014
  packageRoot: path.dirname(resolvedPackageJsonPath),
5638
6015
  error: null,
5639
6016
  };
@@ -5668,6 +6045,20 @@ function resolveGroth16PackageRoot() {
5668
6045
  return path.dirname(findPackageJsonForName(path.dirname(publicDriveCrsPath), "@tokamak-private-dapps/groth16"));
5669
6046
  }
5670
6047
 
6048
+ function readGroth16PackageCompatibleBackendVersion(packageRoot = resolveActiveGroth16PackageRoot()) {
6049
+ return readGroth16CompatibleBackendVersionFromPackageJson(
6050
+ readJson(path.join(packageRoot, "package.json")),
6051
+ GROTH16_PACKAGE_NAME,
6052
+ );
6053
+ }
6054
+
6055
+ function readTokamakCliPackageCompatibleBackendVersion(packageRoot = resolveActiveTokamakCliPackageRoot()) {
6056
+ return readTokamakZkEvmCompatibleBackendVersionFromPackageJson(
6057
+ readJson(path.join(packageRoot, "package.json")),
6058
+ TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
6059
+ );
6060
+ }
6061
+
5671
6062
  function resolveActiveGroth16PackageRoot() {
5672
6063
  const manifestPackageRoot = readPrivateStateCliInstallManifest()?.install?.groth16Runtime?.packageRoot;
5673
6064
  if (manifestPackageRoot && fs.existsSync(path.join(manifestPackageRoot, "package.json"))) {
@@ -5692,17 +6083,24 @@ function inspectGroth16Runtime({ packageRoot = resolveActiveGroth16PackageRoot()
5692
6083
  const report = parseJsonReport(stdout);
5693
6084
  const workspaceRoot = report?.workspaceRoot ?? defaultGroth16WorkspaceRoot();
5694
6085
  const workspaceManifest = readJsonIfExists(path.join(workspaceRoot, "install-manifest.json"));
6086
+ const crsVersion = workspaceManifest?.crs?.version ?? null;
5695
6087
  const packageReport = readPackageReport({
5696
6088
  name: GROTH16_PACKAGE_NAME,
5697
6089
  packageJsonPath: path.join(packageRoot, "package.json"),
5698
6090
  });
6091
+ const compatibleBackendVersion = readGroth16PackageCompatibleBackendVersion(packageRoot);
6092
+ const crsCompatibleBackendVersion = crsVersion
6093
+ ? requireCanonicalGroth16CompatibleBackendVersion(crsVersion, "installed Groth16 CRS version")
6094
+ : null;
5699
6095
  return {
5700
6096
  installed: doctor.status === 0 && report?.ok === true,
5701
6097
  packageVersion: packageReport.version,
6098
+ compatibleBackendVersion,
5702
6099
  packageRoot,
5703
6100
  entryPath,
5704
6101
  workspaceRoot: report?.workspaceRoot ?? null,
5705
- crsVersion: workspaceManifest?.crs?.version ?? null,
6102
+ crsVersion,
6103
+ crsCompatibleBackendVersion,
5706
6104
  crs: workspaceManifest?.crs ?? null,
5707
6105
  checks: report?.checks ?? [],
5708
6106
  doctor: {
@@ -5746,6 +6144,7 @@ function requireActiveTokamakCliRuntimeRoot() {
5746
6144
 
5747
6145
  function inspectTokamakCliRuntime({ packageRoot = resolveActiveTokamakCliPackageRoot() } = {}) {
5748
6146
  const invocation = buildTokamakCliInvocationForPackageRoot(packageRoot);
6147
+ const packageReport = readTokamakCliPackageReport(invocation.packageRoot);
5749
6148
  const doctor = runCaptured(invocation.command, [...invocation.args, "--doctor"], {
5750
6149
  cwd: invocation.packageRoot,
5751
6150
  });
@@ -5762,10 +6161,9 @@ function inspectTokamakCliRuntime({ packageRoot = resolveActiveTokamakCliPackage
5762
6161
  entryPath: invocation.entryPath,
5763
6162
  cacheRoot,
5764
6163
  runtimeRoot,
5765
- packageVersion: readPackageReport({
5766
- name: TOKAMAK_ZKEVM_CLI_PACKAGE_NAME,
5767
- packageJsonPath: path.join(invocation.packageRoot, "package.json"),
5768
- }).version,
6164
+ packageVersion: packageReport.version,
6165
+ compatibleBackendVersion: packageReport.compatibleBackendVersion,
6166
+ packageError: packageReport.error,
5769
6167
  dockerModeInstalled,
5770
6168
  cudaCompatible,
5771
6169
  doctor: {