@tokamak-private-dapps/private-state-cli 2.0.0 → 2.1.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.1.1 - 2026-05-14
6
+
7
+ - Changed `channel recover-workspace --from-genesis` to move any existing local channel workspace
8
+ to `workspace-rebuild-backups/` before writing the current-format workspace. The clean rebuild
9
+ path is limited to workspace files and preserves local account and wallet key secrets under
10
+ `secrets/`.
11
+ - Added channel workspace recovery checkpointing at the existing RPC log chunk boundary so
12
+ interrupted RPC recovery can resume from the last completed chunk.
13
+ - Changed mirror recovery to fall back to a newer verified full mirror checkpoint when no matching
14
+ delta bundle exists for the local recovery index.
15
+ - Changed `wallet recover-workspace` to use the same bounded channel-workspace freshness preflight
16
+ as other wallet commands. `wallet recover-workspace --from-genesis` now restarts received-note
17
+ scanning from channel genesis but does not rebuild the channel workspace from genesis.
18
+ - Added received-note recovery checkpointing at the existing RPC log chunk boundary so ordinary
19
+ `wallet recover-workspace` resumes from the last completed chunk after an interruption.
20
+
21
+ ## 2.1.0 - 2026-05-14
22
+
23
+ - Required current epoch-aware wallet workspaces for wallet commands and backup imports. Local
24
+ wallet metadata must include the wallet index and epoch metadata; users with older local
25
+ workspaces should rebuild them with `wallet recover-workspace`.
26
+ - Required current epoch-aware evidence bundle note paths in the investigator and removed the
27
+ special-case legacy evidence layout branch.
28
+ - Consolidated the static evidence investigator into the CLI package under `cli/investigator/`
29
+ and removed the duplicate top-level investigator copy.
30
+ - Simplified wallet command argument validation by removing unused schema fallback wrappers.
31
+
5
32
  ## 2.0.0 - 2026-05-13
6
33
 
7
34
  - Split wallet export/import into `wallet export backup`, `wallet export viewing-key`,
@@ -22,6 +49,10 @@
22
49
  commitments and nullifiers, creation/spend transaction references, receipts, events, calldata, and
23
50
  filtering indexes, while excluding viewing keys, spending keys, wallet secrets, account private
24
51
  keys, and `.key` files.
52
+ - Made local wallet workspaces epoch-aware. `channel exit` now marks the active wallet epoch as
53
+ exited and retains its local note metadata instead of deleting the wallet workspace, while
54
+ `wallet recover-workspace` and `wallet get-notes --export-evidence` can still use retained exited
55
+ epochs for historical disclosure.
25
56
  - Added a local static evidence investigator GUI and bundled it with the NPM package. The new
26
57
  top-level `private-state-cli investigator` command prints the bundled HTML path, prints the file
27
58
  URL, and opens the GUI in the default browser.
package/README.md CHANGED
@@ -151,16 +151,25 @@ Static warning scope:
151
151
  centralized-exchange controlled address as a self-custody bridge source or as the direct bridge withdrawal target
152
152
  unless the user explicitly understands the compliance implications. Prefer a self-custody L1 wallet.
153
153
 
154
- Workspace recovery commands use the saved recovery index by default. If the local workspace is missing, corrupted, or
155
- does not contain a usable index, `channel recover-workspace` and `wallet recover-workspace` stop with an explicit error instead of
156
- silently replaying logs from channel genesis. Use `--source rpc --from-genesis` only when you intentionally want to
157
- rebuild channel workspace state from the channel creation block:
154
+ Workspace recovery commands use saved recovery indexes by default. If the local channel workspace is missing,
155
+ corrupted, or does not contain a usable index, `channel recover-workspace` stops with an explicit error instead of
156
+ silently replaying logs from channel genesis. Use `channel recover-workspace --source rpc --from-genesis` only when
157
+ you intentionally want to rebuild channel workspace state from the channel creation block:
158
158
 
159
159
  ```bash
160
160
  private-state-cli channel recover-workspace --channel-name <CHANNEL> --network mainnet --source rpc --from-genesis
161
- private-state-cli wallet recover-workspace --channel-name <CHANNEL> --network mainnet --account <ACCOUNT> --from-genesis
162
161
  ```
163
162
 
163
+ When `channel recover-workspace --from-genesis` is used, the CLI treats the local channel workspace as a clean rebuild target.
164
+ If `~/tokamak-private-channels/workspace/<network>/<channel>` already exists, it is moved under
165
+ `~/tokamak-private-channels/workspace-rebuild-backups/` before the current-format workspace is
166
+ created. This backup step is workspace-only; files under `~/tokamak-private-channels/secrets/`,
167
+ including account private keys and wallet viewing/spending key files, are not removed.
168
+ During RPC recovery, the CLI writes a usable channel workspace checkpoint after each RPC log chunk. If an RPC recovery
169
+ run is interrupted, the next non-`--from-genesis` RPC recovery resumes from the last completed chunk. Mirror recovery
170
+ can also start from that local checkpoint: it uses a matching delta bundle when one is available, otherwise a newer
171
+ verified full mirror checkpoint replaces the local checkpoint before RPC catch-up.
172
+
164
173
  `channel create` is the exception: after the channel is created on-chain, the CLI initializes that new local workspace
165
174
  by replaying from the channel's genesis block because no prior recovery index can exist for a new channel.
166
175
 
@@ -169,12 +178,28 @@ registration transaction. For a channel that was created elsewhere, run `channel
169
178
  once before joining, or recover from a registered workspace mirror; later joins and wallet commands resume from the
170
179
  saved index instead of silently replaying from genesis.
171
180
 
172
- Wallet getter commands that need channel state, including `wallet get-meta`, `wallet get-channel-fund`, and
173
- `wallet get-notes`, refresh stale local workspaces through saved recovery indexes before reading state. `wallet get-notes`
174
- also refreshes received-note logs through the saved wallet note recovery index. Automatic refresh never replays from
175
- channel genesis and only runs when the recovery delta fits within the 7,200-block pre-command budget. If a saved index
176
- is missing, unusable, or too far behind, the command stops and asks the user to run the appropriate recovery command
177
- with `--from-genesis` explicitly when needed.
181
+ Wallet commands that need channel state, including `wallet recover-workspace`, `wallet get-meta`,
182
+ `wallet get-channel-fund`, and `wallet get-notes`, refresh stale local channel workspaces through saved recovery
183
+ indexes before reading state. `wallet get-notes` and `wallet recover-workspace` also refresh received-note logs
184
+ through the saved wallet note recovery index. Automatic refresh never replays from channel genesis and only runs when
185
+ the recovery delta fits within the 7,200-block pre-command budget. If a saved index is missing, unusable, or too far
186
+ behind, the command stops and asks the user to run the appropriate recovery command first.
187
+
188
+ Wallet note-delivery recovery checkpoints after each RPC log chunk by updating
189
+ `noteReceiveLastScannedBlock`. If an ordinary `wallet recover-workspace` run is interrupted during note recovery, the
190
+ next run resumes from the last completed chunk. This does not add a special resume path for
191
+ `wallet recover-workspace --from-genesis`; that command intentionally starts received-note scanning from channel
192
+ genesis after the channel workspace has passed the same freshness preflight used by other wallet commands.
193
+
194
+ Local wallet workspaces are epoch-aware. Each successful channel registration creates a wallet epoch under the
195
+ canonical wallet directory. `channel exit` does not delete the local wallet workspace; it marks the active epoch as
196
+ exited with the exit transaction, block, and timestamp, then keeps that epoch read-only for historical note inspection
197
+ and evidence export. If the same account later joins the same channel again, the new registration is a separate active
198
+ epoch under the same canonical wallet name.
199
+
200
+ The current CLI only supports this epoch-aware wallet workspace layout. If a local wallet directory was created by an
201
+ older CLI and does not contain `wallet-index.metadata.json` plus `epochs/<epoch-id>/` metadata files, rebuild it with
202
+ `wallet recover-workspace` before using wallet commands.
178
203
 
179
204
  Channel leaders can optionally register a workspace mirror server so users can bootstrap recovery
180
205
  from a signed checkpoint and download only the local-to-checkpoint delta when a local recovery index
@@ -228,8 +253,9 @@ private-state-cli wallet get-notes --network mainnet --wallet <WALLET> --export-
228
253
  This ZIP is an input for `private-state-cli investigator`. It contains plaintext for all locally known
229
254
  notes, derived commitments and nullifiers, creation and spend transaction references, transaction calldata, receipts,
230
255
  events, and indexes for filtering by note, nullifier, transaction, block range, or available counterparty metadata. It
231
- does not include viewing keys, spending keys, wallet secret material, account private keys, or `.key` files. Do not
232
- submit the raw ZIP as an exchange or auditor package unless full wallet-history disclosure is intended.
256
+ includes all local epochs for the selected wallet, including exited epochs retained after `channel exit`. It does not
257
+ include viewing keys, spending keys, wallet secret material, account private keys, or `.key` files. Do not submit the
258
+ raw ZIP as an exchange or auditor package unless full wallet-history disclosure is intended.
233
259
 
234
260
  Open the local evidence investigator with:
235
261
 
@@ -241,6 +267,9 @@ The command prints the bundled investigator HTML path and file URL, then opens t
241
267
  evidence ZIP in that GUI, apply the requested filters, and export a narrower user-consent disclosure ZIP. The GUI runs
242
268
  locally in the browser and does not send files over the network.
243
269
 
270
+ The investigator accepts current epoch-aware evidence bundles only. If a bundle was generated by an older CLI, rebuild
271
+ the wallet workspace with `wallet recover-workspace` and export a new bundle with `wallet get-notes --export-evidence`.
272
+
244
273
  Estimate live transaction costs before sending commands with:
245
274
 
246
275
  ```bash
@@ -298,7 +327,8 @@ and stores a protected local account secret for later `--account` use. The sourc
298
327
  wallet backup metadata, viewing-key metadata, and spending-key metadata as separate files. `wallet list` reads only the local workspace and prints saved wallet names that can be reused with
299
328
  `--wallet`.
300
329
  `wallet get-meta` opens the wallet metadata and reports the stored L1/L2 identity metadata plus the current
301
- on-chain channel registration match state, including the registered note-receive public key when present.
330
+ on-chain channel registration match state, including the registered note-receive public key when present. On
331
+ epoch-aware wallet workspaces it also reports the selected wallet epoch and whether that epoch is active or exited.
302
332
  `account get-l1-address` is a simple offline helper that derives the L1 address for a local account.
303
333
 
304
334
  ### Wallet Secret Source File
@@ -404,6 +434,12 @@ Operating rules:
404
434
  - A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
405
435
  without replaying the channel from its creation block. If it is missing, explain `--from-genesis` before using it
406
436
  because genesis replay can take much longer.
437
+ - Before guiding a user to run `channel recover-workspace --source rpc --from-genesis`, explain that RPC genesis
438
+ recovery can be very slow because it scans channel logs from the creation block. If a channel workspace mirror is
439
+ available, try mirror-based recovery first, and use RPC genesis replay only when mirror recovery is unavailable or
440
+ unsuitable.
441
+ - When a CLI command fails, read the error message and any printed `Try:` hints first. Prefer the corrective action
442
+ suggested by the CLI before inventing a different recovery sequence.
407
443
  - When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
408
444
  selected network. They can obtain one from an infrastructure provider such as Alchemy, Infura, QuickNode, or from
409
445
  their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only the endpoint URL
@@ -466,7 +502,7 @@ Suggested interaction flow:
466
502
  `wallet mint-notes`.
467
503
  7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
468
504
  `wallet get-meta`, then build `wallet transfer-notes`.
469
- 8. After transfer, guide the recipient to run `wallet get-notes`; it refreshes received notes from the saved recovery index when the delta fits the 7,200-block pre-command budget. If the index is missing or too far behind, explain `wallet recover-workspace --from-genesis`.
505
+ 8. After transfer, guide the recipient to run `wallet get-notes`; it refreshes received notes from the saved recovery index when the delta fits the 7,200-block pre-command budget. If the index is missing or too far behind, explain `wallet recover-workspace`.
470
506
 
471
507
  Example onboarding explanation for `channel join`:
472
508
 
@@ -22,6 +22,10 @@ The raw evidence bundle contains plaintext for all locally known notes. Do not s
22
22
  as an exchange or auditor package unless full wallet-history disclosure is intended. Use the
23
23
  investigator output package for scoped disclosure.
24
24
 
25
+ The investigator accepts current epoch-aware evidence bundles only. Supported note records live under
26
+ `wallets/<wallet>/epochs/<epoch-id>/notes/` inside the ZIP. If a bundle uses an older layout, rebuild the local wallet
27
+ workspace with `wallet recover-workspace` and export a new evidence ZIP with `wallet get-notes --export-evidence`.
28
+
25
29
  ## Supported Filtering
26
30
 
27
31
  - note commitment or nullifier
@@ -60,8 +60,11 @@ async function loadEvidenceBundle(bytes) {
60
60
  if (manifest.format !== "tokamak-private-state-raw-evidence-bundle") {
61
61
  throw new Error(`Unsupported evidence format: ${manifest.format ?? "missing"}.`);
62
62
  }
63
+ if (Number(manifest.formatVersion) !== 2) {
64
+ throw new Error("Current evidence bundle formatVersion 2 is required. Run wallet recover-workspace, then run wallet get-notes --export-evidence again.");
65
+ }
63
66
  const notes = [...files.entries()]
64
- .filter(([path]) => path.startsWith("notes/") && path.endsWith(".json"))
67
+ .filter(([path]) => isEvidenceNotePath(path))
65
68
  .map(([path, content]) => ({
66
69
  path,
67
70
  record: JSON.parse(content),
@@ -69,7 +72,7 @@ async function loadEvidenceBundle(bytes) {
69
72
  .sort((left, right) =>
70
73
  String(left.record?.derived?.commitment ?? "").localeCompare(String(right.record?.derived?.commitment ?? "")));
71
74
  if (notes.length === 0) {
72
- throw new Error("The evidence bundle does not contain note records.");
75
+ throw new Error("The evidence bundle does not contain current epoch-aware note records.");
73
76
  }
74
77
  state.files = files;
75
78
  state.manifest = manifest;
@@ -79,7 +82,20 @@ async function loadEvidenceBundle(bytes) {
79
82
  els.buildPackage.disabled = false;
80
83
  els.selectAll.disabled = false;
81
84
  els.selectNone.disabled = false;
82
- setStatus(`Loaded ${notes.length} note records from ${manifest.wallet ?? "wallet"} on ${manifest.network ?? "network"}.`);
85
+ setStatus(
86
+ `Loaded ${notes.length} note records from ${evidenceWalletLabel(manifest)} on ${manifest.network ?? "network"}.`,
87
+ );
88
+ }
89
+
90
+ function isEvidenceNotePath(entryPath) {
91
+ return /^wallets\/[^/]+\/epochs\/[^/]+\/notes\/[^/]+\.json$/u.test(entryPath);
92
+ }
93
+
94
+ function evidenceWalletLabel(manifest) {
95
+ if (manifest.wallets?.length) {
96
+ return `${manifest.wallets[0].wallet ?? manifest.wallet ?? "wallet"} (${manifest.wallets.length} epochs)`;
97
+ }
98
+ return manifest.wallet ?? "wallet";
83
99
  }
84
100
 
85
101
  function resetBundle() {
@@ -324,6 +340,7 @@ function buildDisclosureManifest({ selectedNotes, selectedPaths, packageMetadata
324
340
  channelName: state.manifest.channelName,
325
341
  channelId: state.manifest.channelId,
326
342
  wallet: state.manifest.wallet,
343
+ wallets: state.manifest.wallets ?? null,
327
344
  walletL1Address: state.manifest.walletL1Address,
328
345
  walletL2Address: state.manifest.walletL2Address,
329
346
  },
@@ -337,8 +337,11 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
337
337
  help: [
338
338
  "By default, --source rpc resumes RPC log scanning from the workspace recovery index when available",
339
339
  "--source mirror validates the channel leader's registered checkpoint manifest, downloads only the needed checkpoint or delta bundle, and then replays RPC logs to latest",
340
+ "RPC recovery writes a usable channel workspace checkpoint after each RPC log chunk, so interrupted runs can resume from the last completed chunk",
341
+ "Mirror recovery uses a matching delta bundle when available; otherwise a newer verified full checkpoint replaces the local checkpoint before RPC catch-up",
340
342
  "Fails instead of falling back to genesis when no usable recovery index exists",
341
343
  "Use --source rpc --from-genesis to ignore the recovery index and replay logs from channel genesis",
344
+ "--from-genesis moves the existing local channel workspace to workspace-rebuild-backups before writing the current-format workspace; local secrets are preserved",
342
345
  "Prints RPC log scan progress while rebuilding the workspace",
343
346
  ],
344
347
  },
@@ -410,10 +413,11 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
410
413
  help: [
411
414
  "Rebuilds backup metadata from channel state without recreating the spending key",
412
415
  "Derives and stores the viewing key when the local account signer can reproduce the registered viewing public key",
413
- "By default, resumes RPC log scanning from the workspace recovery index when available",
414
- "Fails instead of falling back to genesis when no usable recovery index exists",
415
- "Use --from-genesis to ignore the recovery index and replay channel logs from channel genesis",
416
- "Prints RPC log scan progress while rebuilding channel state and received-note state",
416
+ "Before wallet recovery, refreshes stale channel workspace state only when the saved recovery index delta fits the pre-command budget",
417
+ "Fails and asks for channel recover-workspace first when the channel workspace is missing, unusable, or too stale for automatic recovery",
418
+ "Use --from-genesis to restart received-note scanning from channel genesis; it does not rebuild the channel workspace from genesis",
419
+ "Received-note recovery checkpoints after each RPC log chunk during ordinary recovery; --from-genesis still starts received-note scanning from channel genesis",
420
+ "Prints RPC log scan progress while refreshing channel state and rebuilding received-note state",
417
421
  ],
418
422
  },
419
423
  {
@@ -441,7 +445,10 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
441
445
  description: "Check whether a wallet matches the on-chain channel registration.",
442
446
  fields: ["wallet", "network"],
443
447
  usage: "--wallet and --network",
444
- help: ["Refreshes the local channel workspace through the saved recovery index before reading registration metadata when the scan fits the 10 second pre-command budget"],
448
+ help: [
449
+ "Refreshes the local channel workspace through the saved recovery index before reading registration metadata when the scan fits the 10 second pre-command budget",
450
+ "Reports the selected local wallet epoch and lifecycle status when the workspace uses the epoch-aware wallet format",
451
+ ],
445
452
  },
446
453
  {
447
454
  id: "wallet-list",
@@ -548,7 +555,10 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
548
555
  description: "Exit a channel. Both the CLI and bridge contract require a zero channel balance.",
549
556
  fields: ["wallet", "network"],
550
557
  usage: "--wallet and --network",
551
- help: ["Refreshes the local channel workspace through the saved recovery index before checking the channel balance when the scan fits the 10 second pre-command budget"],
558
+ help: [
559
+ "Refreshes the local channel workspace through the saved recovery index before checking the channel balance when the scan fits the 10 second pre-command budget",
560
+ "Marks the current local wallet epoch as exited and keeps its note metadata available for historical evidence export",
561
+ ],
552
562
  },
553
563
  {
554
564
  id: "wallet-mint-notes",
@@ -613,8 +623,9 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
613
623
  help: [
614
624
  "Refreshes the local channel workspace through the saved recovery index before reading notes when the scan fits the 10 second pre-command budget",
615
625
  "Refreshes received-note logs through the saved wallet note recovery index when the scan fits the 10 second pre-command budget",
616
- "Fails instead of replaying from genesis; run wallet recover-workspace --from-genesis when a genesis rebuild is required",
626
+ "Fails instead of replaying from genesis; run wallet recover-workspace first when explicit wallet recovery is required",
617
627
  "Use --export-evidence <PATH> with --acknowledge-full-note-plaintext-export to write a local full-note evidence ZIP for private-state-cli investigator",
628
+ "Evidence export includes all local epochs for the selected wallet, including exited epochs retained for dispute evidence",
618
629
  ],
619
630
  },
620
631
  ]);
@@ -1,7 +1,12 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { AbiCoder, ethers } from "ethers";
3
- import { deriveL2KeysFromSignature, poseidon } from "tokamak-l2js";
3
+ import { deriveL2KeysFromSignature } from "tokamak-l2js";
4
4
  import { jubjub } from "@noble/curves/jubjub";
5
+ import {
6
+ normalizeBytesHex,
7
+ normalizeBytes32Hex,
8
+ poseidonHexFromBytes,
9
+ } from "@tokamak-private-dapps/common-library/tokamak-l2-helpers";
5
10
 
6
11
  const abiCoder = AbiCoder.defaultAbiCoder();
7
12
 
@@ -42,18 +47,6 @@ function expect(condition, message) {
42
47
  }
43
48
  }
44
49
 
45
- function normalizeBytes32Hex(value) {
46
- return ethers.hexlify(ethers.zeroPadValue(ethers.hexlify(value), 32)).toLowerCase();
47
- }
48
-
49
- function normalizeBytes16Hex(value) {
50
- return ethers.hexlify(ethers.zeroPadValue(ethers.hexlify(value), 16)).toLowerCase();
51
- }
52
-
53
- function poseidonHexFromBytes(bytesLike) {
54
- return ethers.hexlify(poseidon(ethers.getBytes(bytesLike))).toLowerCase();
55
- }
56
-
57
50
  function noteReceivePubKeyFromPoint(point) {
58
51
  const affine = point.toAffine();
59
52
  return {
@@ -371,7 +364,7 @@ function decryptFieldEncryptedNoteValue({
371
364
  encryptionInfo,
372
365
  });
373
366
  expect(
374
- normalizeBytes16Hex(expectedTag) === normalizeBytes16Hex(normalized.tag),
367
+ normalizeBytesHex(expectedTag, 16) === normalizeBytesHex(normalized.tag, 16),
375
368
  "Encrypted note value integrity tag mismatch.",
376
369
  );
377
370
  const fieldMask = deriveFieldMask({
@@ -9,7 +9,6 @@ import { fetchNpmPackageMetadata } from "@tokamak-private-dapps/common-library/n
9
9
  import {
10
10
  normalizePackageVersionToCompatibleBackendVersion,
11
11
  readTokamakZkEvmCompatibleBackendVersionFromPackageJson,
12
- requireCanonicalCompatibleBackendVersion,
13
12
  requireExactSemverVersion,
14
13
  } from "@tokamak-private-dapps/common-library/proof-backend-versioning";
15
14
  import {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
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",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@ethereumjs/util": "^10.1.1",
46
46
  "@noble/curves": "1.9.7",
47
- "@tokamak-private-dapps/common-library": "^0.1.1",
47
+ "@tokamak-private-dapps/common-library": "^0.1.2",
48
48
  "@tokamak-private-dapps/groth16": "^0.2.0",
49
49
  "@tokamak-zk-evm/cli": "^2.1.0",
50
50
  "adm-zip": "^0.5.17",