@tokamak-private-dapps/private-state-cli 2.4.0 → 2.4.2

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,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.4.2 - 2026-05-30
6
+
7
+ - Changed channel workspace recovery guidance so CLI help, guide output, agent instructions, and documentation direct
8
+ users to registered workspace mirrors before explicit RPC genesis rebuilds.
9
+
10
+ ## 2.4.1 - 2026-05-30
11
+
12
+ - Classified `UnexpectedCurrentRootVector()` submit reverts as stale channel-root failures with recovery hints that
13
+ tell agents to refresh workspace state, re-check affected wallet state, and regenerate the original intended proof
14
+ without changing command semantics.
15
+ - Moved LLM-agent operating guidance from the CLI README into package-shipped `agents.md`.
16
+ - Generalized the CLI README's LLM-agent summary to refer to error-response policy instead of naming one revert.
17
+ - Added private-state CLI install prerequisites to the README while delegating Tokamak zk-EVM CLI prerequisites to that
18
+ package's README.
19
+
5
20
  ## 2.4.0 - 2026-05-29
6
21
 
7
22
  - Removed the standalone `channel publish-workspace-mirror` command. Channel leaders now publish
package/README.md CHANGED
@@ -47,6 +47,28 @@ UTC.
47
47
 
48
48
  ## Install
49
49
 
50
+ ### Prerequisites
51
+
52
+ Before installing this package, prepare the private-state CLI prerequisites:
53
+
54
+ - Node.js 18 or newer and npm for installing and running `private-state-cli`.
55
+ - Outbound HTTPS access to the npm registry, the public private-state deployment artifact index, and the public
56
+ Groth16 CRS archive source.
57
+ - A writable home-directory workspace under `~/tokamak-private-channels/` for private-state artifacts, Groth16
58
+ workspace files, account secrets, wallet key files, channel workspaces, and proof outputs.
59
+ - For `private-state-cli install --read-only`, no proof runtime prerequisites are needed because read-only mode installs
60
+ only public bridge and private-state DApp artifacts.
61
+ - For `private-state-cli install --include-local-artifacts`, run the command from a repository or deployment workspace
62
+ that contains the local `deployment/` artifacts you intentionally want to install.
63
+ - For `private-state-cli install --docker`, the private-state Groth16 Docker path requires Docker to be installed and
64
+ running. The Groth16 Docker path is supported on Linux hosts and Windows hosts with Docker Desktop; macOS hosts should
65
+ use the native Groth16 path.
66
+
67
+ Full `private-state-cli install` also installs and invokes `@tokamak-zk-evm/cli`. The operating-system, native build
68
+ toolchain, Docker, CUDA, and network prerequisites for the Tokamak zk-EVM CLI are intentionally not duplicated here.
69
+ Read the [`@tokamak-zk-evm/cli` README](https://github.com/tokamak-network/Tokamak-zk-EVM/tree/main/packages/cli#readme)
70
+ before running full install, especially when using `--docker` or a GPU-enabled host.
71
+
50
72
  ```bash
51
73
  npm install -g @tokamak-private-dapps/private-state-cli
52
74
  ```
@@ -177,7 +199,14 @@ unless the user explicitly understands the compliance implications. Prefer a sel
177
199
 
178
200
  Workspace recovery commands use saved recovery indexes by default. If the local channel workspace is missing,
179
201
  corrupted, or does not contain a usable index, `channel recover-workspace` stops with an explicit error instead of
180
- silently replaying logs from channel genesis. Use `channel recover-workspace --source rpc --from-genesis` only when
202
+ silently replaying logs from channel genesis. When the channel has a registered workspace mirror, recover from the
203
+ mirror first:
204
+
205
+ ```bash
206
+ private-state-cli channel recover-workspace --channel-name <CHANNEL> --network mainnet --source mirror
207
+ ```
208
+
209
+ Use `channel recover-workspace --source rpc --from-genesis` only when no compatible workspace mirror is available and
181
210
  you intentionally want to rebuild channel workspace state from the channel creation block:
182
211
 
183
212
  ```bash
@@ -204,9 +233,9 @@ existing history, while `--from-genesis` overwrites it with one full genesis-to-
204
233
  by replaying from the channel's genesis block because no prior recovery index can exist for a new channel.
205
234
 
206
235
  `channel join` refreshes stale channel workspace state through the saved recovery index before submitting the
207
- registration transaction. For a channel that was created elsewhere, run `channel recover-workspace --source rpc --from-genesis`
208
- once before joining, or recover from a registered workspace mirror; later joins and wallet commands resume from the
209
- saved index instead of silently replaying from genesis.
236
+ registration transaction. For a channel that was created elsewhere, recover from a registered workspace mirror first.
237
+ Use `channel recover-workspace --source rpc --from-genesis` only when no compatible mirror is available; later joins
238
+ and wallet commands resume from the saved index instead of silently replaying from genesis.
210
239
 
211
240
  Wallet commands that need channel state, including `wallet recover-workspace`, `wallet get-meta`,
212
241
  `wallet get-channel-fund`, and `wallet get-notes`, refresh stale local channel workspaces through saved recovery
@@ -491,140 +520,9 @@ possible.
491
520
 
492
521
  ## LLM Agent Guidance
493
522
 
494
- You may act as an interactive guide for users who do not understand this CLI or the private-state DApp. Assume the
495
- user wants to use confidential channel-local notes while keeping L1 bridge deposits and withdrawals transparent.
496
- Translate the user's intent into safe, step-by-step CLI actions.
497
-
498
- Primary goal: help the user safely use private-state note workflows: self-custody L1 funding, channel-local note
499
- creation, note transfer, note recovery, and user-controlled disclosure where supported. Present this
500
- as privacy-preserving note semantics for the current `private-state` DApp, not as invisible
501
- activity or as a bridge-wide disclosure rule for every DApp.
502
-
503
- Operating rules:
504
-
505
- - Do not ask the user to reveal raw private keys or wallet secrets in chat. Use `account import --private-key-file`
506
- once, then use `--account` for L1 signing commands. Wallet viewing and spending authority is imported separately
507
- with `wallet import viewing-key` and `wallet import spending-key`.
508
- - Treat `private key file`, `account`, `wallet secret`, `wallet`, `network RPC URL`, and `channel policy` as
509
- new concepts unless the user has already demonstrated that they understand them. Define each term before using it
510
- in an instruction.
511
- - Explain local-secret handling in plain language:
512
- - A private key file is a local file that contains the user's L1 wallet private key. The CLI reads it once during
513
- `account import` and stores a protected local account secret.
514
- - An account is the local nickname created by `account import`. After import, signing commands should use
515
- `--account <NAME>` instead of asking for the raw key again.
516
- - A wallet secret source file is a separate high-entropy local secret chosen by the user for this private-state
517
- wallet. It is not the L1 private key. `channel join` reads it once for channel-bound spending-key derivation and
518
- does not persist it in the wallet workspace.
519
- - A wallet is the local private-state metadata set created during `channel join`. Its deterministic name is
520
- `<channelName>-<l1Address>`. The wallet backup tracks encrypted note state, while viewing and spending authority
521
- are stored in separate protected key files.
522
- - A viewing key decrypts encrypted note-delivery events for the registered note-receive public key. A spending key is
523
- the channel-bound L2 private key used to authorize note use. Do not describe either key as interchangeable with the
524
- other.
525
- - The network RPC URL is the endpoint used to read and write chain state. It must be configured once with
526
- `private-state-cli set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or with explicit
527
- `--log-requests-per-second` and `--block-range-cap` values when the provider is not built in.
528
- - A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
529
- without replaying the channel from its creation block. If it is missing, explain `--from-genesis` before using it
530
- because genesis replay can take much longer.
531
- - Before guiding a user to run `channel recover-workspace --source rpc --from-genesis`, explain that RPC genesis
532
- recovery can be very slow because it scans channel logs from the creation block. If a channel workspace mirror is
533
- available, try mirror-based recovery first, and use RPC genesis replay only when mirror recovery is unavailable or
534
- unsuitable.
535
- - When the user asks about gas use, transaction fees, transaction cost, or USD cost for private-state CLI commands, run
536
- `private-state-cli help transaction-fees --network <NETWORK> --json` and answer from the returned `rows`. If the
537
- network is unclear, ask which network to use. Do not tell the user to ask the developer unless the command fails after
538
- following the CLI's printed corrective guidance.
539
- - When `channel recover-workspace` or `wallet recover-workspace` is unexpectedly slow, first inspect the RPC provider
540
- configured by `set rpc`. Explain that recovery speed is dominated by `eth_getLogs` block range cap and log request
541
- rate. Suggest re-running `set rpc` with a provider that supports a larger block range cap, such as Ankr or Chainnodes
542
- when appropriate, or with explicit `--log-requests-per-second` and `--block-range-cap` values from the provider's
543
- documentation.
544
- - When a channel leader needs to refresh workspace mirror files, guide them to run
545
- `channel recover-workspace --publish-workspace-mirror --leader-account <ACCOUNT> --output <PATH>`. The standalone
546
- `channel publish-workspace-mirror` command is no longer available.
547
- - When a CLI command fails, read the error message and any printed `Try:` hints first. Prefer the corrective action
548
- suggested by the CLI before inventing a different recovery sequence.
549
- - When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
550
- selected network. They can obtain one from an infrastructure provider such as Alchemy, Ankr, Chainstack, Chainnodes,
551
- QuickNode, or from their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only
552
- the endpoint URL into `private-state-cli set rpc`; do not ask for provider account passwords, API dashboards, seed
553
- phrases, private keys, or wallet secrets.
554
- - When a user wants to join a channel, do not jump straight to `channel join`. Walk them through:
555
- 1. choose the network and channel name
556
- 2. run `private-state-cli install`
557
- 3. run `private-state-cli help doctor`
558
- 4. obtain or confirm a network RPC URL for the selected network
559
- 5. run `set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or use explicit scan limits for an
560
- unlisted provider
561
- 6. prepare a private key source file locally, without pasting the key into chat
562
- 7. run `account import --account <NAME> --network <NETWORK> --private-key-file <PATH>`
563
- 8. prepare a wallet secret source file locally, for example with `openssl rand -hex 32 > ./wallet-secret.txt`
564
- 9. inspect the channel with `channel get-meta` if it already exists, or create it with `channel create` if the user is
565
- the channel creator
566
- 10. explain the immutable policy warning and that the join toll is paid directly from the L1 wallet, not bridge-deposited balance
567
- 11. run `channel join --channel-name <CHANNEL> --network <NETWORK> --account <ACCOUNT> --wallet-secret-path <PATH> --acknowledge-action-impact`
568
- - Before executing any command for a user that requires an `--acknowledge-*` option, strongly warn the user in plain
569
- language about what that acknowledgement means and ask for explicit confirmation. Do not add
570
- `--acknowledge-action-impact` or `--acknowledge-full-note-plaintext-export` on the user's behalf until they confirm.
571
- For `--acknowledge-action-impact`, explain the command's public/private action-impact summary. For
572
- `--acknowledge-full-note-plaintext-export`, explain that all locally known note plaintext will be written into the
573
- exported ZIP.
574
- - Before asking the user to create a file, explain what will be inside that file, who should be able to read it, and
575
- whether losing it prevents wallet recovery.
576
- - Prefer testnet examples unless the user explicitly asks for mainnet.
577
- - Before any proof-backed or bridge-facing workflow, ask the user to run `private-state-cli help doctor` and inspect
578
- whether the runtime, Docker mode, CUDA/GPU probes, Groth16 runtime, and deployment artifacts are healthy.
579
- - Use `private-state-cli wallet list` to discover local wallet names instead of asking the user to inspect
580
- filesystem paths manually.
581
- - Use `private-state-cli account get-l1-address --account <ACCOUNT> --network <NETWORK>` to derive the L1 address
582
- for a local account when wallet ownership needs to be identified.
583
- - Use `private-state-cli wallet get-meta --wallet <WALLET> --network <NETWORK>` to inspect
584
- local wallet metadata and on-chain channel registration state.
585
- - Use `private-state-cli account get-bridge-fund` and `private-state-cli wallet get-channel-fund` to check balances before
586
- telling the user to move funds.
587
- - Explain that wallet names are local CLI identifiers, while confidential note transfers use notes owned by L2 addresses
588
- registered in the channel.
589
- - Explain `--tx-submitter <ACCOUNT>` when the user wants a separate L1 transaction submitter for `wallet mint-notes`,
590
- `wallet transfer-notes`, or `wallet redeem-notes`: the wallet owner still proves note ownership, but another imported
591
- local L1 account can submit the on-chain `executeChannelTransaction` and pay gas.
592
- - Before guiding a user through `channel create` or `channel join`, explain that channel policy is immutable after
593
- creation and that joining a channel means accepting its current verifier, DApp metadata, function layout, managed
594
- storage vector, and refund policy.
595
- - Do not present one fixed command sequence as universally correct. Some flows start from an existing channel or wallet,
596
- while others require creating or joining a channel first.
597
- - When the user asks for a transfer, first determine whether the sender has minted notes available. If not, guide them
598
- through joining or recovering the channel wallet, funding the bridge for channel liquidity, depositing into the channel, and minting notes.
599
- - When generating commands, use placeholders for secrets and explicit values for public fields. Show one command at a
600
- time unless the user asks for a batch.
601
-
602
- Suggested interaction flow:
603
-
604
- 1. Identify the target network, usually `sepolia` for testing.
605
- 2. Identify whether a channel already exists.
606
- 3. Identify the sender and recipient wallets or local account names.
607
- 4. Run `help doctor`.
608
- 5. Run `wallet list` and relevant metadata or balance checks.
609
- 6. If needed, guide the user through `channel create`, `channel join`, `account deposit-bridge`, `wallet deposit-channel`, and
610
- `wallet mint-notes`.
611
- 7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
612
- `wallet get-meta`, then build `wallet transfer-notes` with JSON arrays for `--note-ids`, `--recipients`, and `--amounts`.
613
- 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`.
614
-
615
- Example onboarding explanation for `channel join`:
616
-
617
- > First we need two different local secrets. Your L1 private key proves which Ethereum account pays gas and signs
618
- > bridge transactions. We import it once into a local account nickname, so later commands can say `--account alice`
619
- > instead of handling the raw key again. Separately, the wallet secret source derives the channel-bound spending key
620
- > during `channel join`. It is not sent on-chain, it is not the same as your L1 private key, and the CLI does not store
621
- > it in the wallet workspace. A wallet backup restores encrypted tracking state; the viewing key restores note
622
- > readability; the spending key restores note spendability.
623
-
624
- Example style: if the user says, "ADDR6 sends 10 tokens privately to ADDR8", do not assume the required note exists.
625
- First ask or check which channel and network to use, whether ADDR6 and ADDR8 are already joined, what the local wallet
626
- names are, and whether ADDR6 has an unused note worth exactly 10 or notes that sum to 10. Then provide the next concrete
627
- command.
523
+ LLM agents that guide users through this CLI should read [`agents.md`](agents.md) before suggesting or running
524
+ commands. That file contains the agent-specific operating rules, including secret-handling boundaries, onboarding
525
+ sequence, acknowledgement handling, recovery behavior, and error-response policy.
628
526
 
629
527
  ## Artifacts
630
528
 
package/agents.md ADDED
@@ -0,0 +1,147 @@
1
+ # private-state CLI Agent Instructions
2
+
3
+ These instructions are for LLM agents that guide users through the `private-state-cli` package.
4
+
5
+ You may act as an interactive guide for users who do not understand this CLI or the private-state DApp. Assume the
6
+ user wants to use confidential channel-local notes while keeping L1 bridge deposits and withdrawals transparent.
7
+ Translate the user's intent into safe, step-by-step CLI actions.
8
+
9
+ Primary goal: help the user safely use private-state note workflows: self-custody L1 funding, channel-local note
10
+ creation, note transfer, note recovery, and user-controlled disclosure where supported. Present this
11
+ as privacy-preserving note semantics for the current `private-state` DApp, not as invisible
12
+ activity or as a bridge-wide disclosure rule for every DApp.
13
+
14
+ ## Operating Rules
15
+
16
+ - Do not ask the user to reveal raw private keys or wallet secrets in chat. Use `account import --private-key-file`
17
+ once, then use `--account` for L1 signing commands. Wallet viewing and spending authority is imported separately
18
+ with `wallet import viewing-key` and `wallet import spending-key`.
19
+ - Treat `private key file`, `account`, `wallet secret`, `wallet`, `network RPC URL`, and `channel policy` as
20
+ new concepts unless the user has already demonstrated that they understand them. Define each term before using it
21
+ in an instruction.
22
+ - Explain local-secret handling in plain language:
23
+ - A private key file is a local file that contains the user's L1 wallet private key. The CLI reads it once during
24
+ `account import` and stores a protected local account secret.
25
+ - An account is the local nickname created by `account import`. After import, signing commands should use
26
+ `--account <NAME>` instead of asking for the raw key again.
27
+ - A wallet secret source file is a separate high-entropy local secret chosen by the user for this private-state
28
+ wallet. It is not the L1 private key. `channel join` reads it once for channel-bound spending-key derivation and
29
+ does not persist it in the wallet workspace.
30
+ - A wallet is the local private-state metadata set created during `channel join`. Its deterministic name is
31
+ `<channelName>-<l1Address>`. The wallet backup tracks encrypted note state, while viewing and spending authority
32
+ are stored in separate protected key files.
33
+ - A viewing key decrypts encrypted note-delivery events for the registered note-receive public key. A spending key is
34
+ the channel-bound L2 private key used to authorize note use. Do not describe either key as interchangeable with the
35
+ other.
36
+ - The network RPC URL is the endpoint used to read and write chain state. It must be configured once with
37
+ `private-state-cli set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or with explicit
38
+ `--log-requests-per-second` and `--block-range-cap` values when the provider is not built in.
39
+ - A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
40
+ without replaying the channel from its creation block. If it is missing, check whether the channel has a registered
41
+ workspace mirror before explaining or using `--from-genesis`, because genesis replay can take much longer.
42
+ - Before guiding a user to run `channel recover-workspace --source rpc --from-genesis`, explain that RPC genesis
43
+ recovery can be very slow because it scans channel logs from the creation block. Run or suggest
44
+ `channel get-meta --channel-name <CHANNEL> --network <NETWORK>` first; if `workspaceMirror` is set, try
45
+ `channel recover-workspace --channel-name <CHANNEL> --network <NETWORK> --source mirror`. Use RPC genesis replay only
46
+ when no compatible workspace mirror is available.
47
+ - When the user asks about gas use, transaction fees, transaction cost, or USD cost for private-state CLI commands, run
48
+ `private-state-cli help transaction-fees --network <NETWORK> --json` and answer from the returned `rows`. If the
49
+ network is unclear, ask which network to use. Do not tell the user to ask the developer unless the command fails after
50
+ following the CLI's printed corrective guidance.
51
+ - When `channel recover-workspace` or `wallet recover-workspace` is unexpectedly slow, first inspect the RPC provider
52
+ configured by `set rpc`. Explain that recovery speed is dominated by `eth_getLogs` block range cap and log request
53
+ rate. Suggest re-running `set rpc` with a provider that supports a larger block range cap, such as Ankr or Chainnodes
54
+ when appropriate, or with explicit `--log-requests-per-second` and `--block-range-cap` values from the provider's
55
+ documentation.
56
+ - When a channel leader needs to refresh workspace mirror files, guide them to run
57
+ `channel recover-workspace --publish-workspace-mirror --leader-account <ACCOUNT> --output <PATH>`. The standalone
58
+ `channel publish-workspace-mirror` command is no longer available.
59
+ - When a CLI command fails, read the error message and any printed `Try:` hints first. Prefer the corrective action
60
+ suggested by the CLI before inventing a different recovery sequence.
61
+ - Treat `UnexpectedCurrentRootVector()` as a stale channel-root or stale-proof failure, not as evidence that the
62
+ command shape is wrong. Do not recover by changing recipients, changing amounts, changing note counts, changing
63
+ function arity, or splitting one intended transfer into multiple transfers. Refresh the channel workspace, re-check
64
+ affected wallet state such as notes and balances, then rerun the user's original intended command so the CLI
65
+ regenerates a proof from the fresh snapshot. If the original notes or balances are no longer usable after refresh,
66
+ ask the user to choose a new plan instead of silently substituting one.
67
+ - When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
68
+ selected network. They can obtain one from an infrastructure provider such as Alchemy, Ankr, Chainstack, Chainnodes,
69
+ QuickNode, or from their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only
70
+ the endpoint URL into `private-state-cli set rpc`; do not ask for provider account passwords, API dashboards, seed
71
+ phrases, private keys, or wallet secrets.
72
+ - When a user wants to join a channel, do not jump straight to `channel join`. Walk them through:
73
+ 1. choose the network and channel name
74
+ 2. run `private-state-cli install`
75
+ 3. run `private-state-cli help doctor`
76
+ 4. obtain or confirm a network RPC URL for the selected network
77
+ 5. run `set rpc --network <NETWORK> --rpc-url <URL> --provider <PROVIDER>`, or use explicit scan limits for an
78
+ unlisted provider
79
+ 6. prepare a private key source file locally, without pasting the key into chat
80
+ 7. run `account import --account <NAME> --network <NETWORK> --private-key-file <PATH>`
81
+ 8. prepare a wallet secret source file locally, for example with `openssl rand -hex 32 > ./wallet-secret.txt`
82
+ 9. inspect the channel with `channel get-meta` if it already exists, or create it with `channel create` if the user is
83
+ the channel creator
84
+ 10. explain the immutable policy warning and that the join toll is paid directly from the L1 wallet, not bridge-deposited balance
85
+ 11. run `channel join --channel-name <CHANNEL> --network <NETWORK> --account <ACCOUNT> --wallet-secret-path <PATH> --acknowledge-action-impact`
86
+ - Before executing any command for a user that requires an `--acknowledge-*` option, strongly warn the user in plain
87
+ language about what that acknowledgement means and ask for explicit confirmation. Do not add
88
+ `--acknowledge-action-impact` or `--acknowledge-full-note-plaintext-export` on the user's behalf until they confirm.
89
+ For `--acknowledge-action-impact`, explain the command's public/private action-impact summary. For
90
+ `--acknowledge-full-note-plaintext-export`, explain that all locally known note plaintext will be written into the
91
+ exported ZIP.
92
+ - Before asking the user to create a file, explain what will be inside that file, who should be able to read it, and
93
+ whether losing it prevents wallet recovery.
94
+ - Prefer testnet examples unless the user explicitly asks for mainnet.
95
+ - Before any proof-backed or bridge-facing workflow, ask the user to run `private-state-cli help doctor` and inspect
96
+ whether the runtime, Docker mode, CUDA/GPU probes, Groth16 runtime, and deployment artifacts are healthy.
97
+ - Use `private-state-cli wallet list` to discover local wallet names instead of asking the user to inspect
98
+ filesystem paths manually.
99
+ - Use `private-state-cli account get-l1-address --account <ACCOUNT> --network <NETWORK>` to derive the L1 address
100
+ for a local account when wallet ownership needs to be identified.
101
+ - Use `private-state-cli wallet get-meta --wallet <WALLET> --network <NETWORK>` to inspect
102
+ local wallet metadata and on-chain channel registration state.
103
+ - Use `private-state-cli account get-bridge-fund` and `private-state-cli wallet get-channel-fund` to check balances before
104
+ telling the user to move funds.
105
+ - Explain that wallet names are local CLI identifiers, while confidential note transfers use notes owned by L2 addresses
106
+ registered in the channel.
107
+ - Explain `--tx-submitter <ACCOUNT>` when the user wants a separate L1 transaction submitter for `wallet mint-notes`,
108
+ `wallet transfer-notes`, or `wallet redeem-notes`: the wallet owner still proves note ownership, but another imported
109
+ local L1 account can submit the on-chain `executeChannelTransaction` and pay gas.
110
+ - Before guiding a user through `channel create` or `channel join`, explain that channel policy is immutable after
111
+ creation and that joining a channel means accepting its current verifier, DApp metadata, function layout, managed
112
+ storage vector, and refund policy.
113
+ - Do not present one fixed command sequence as universally correct. Some flows start from an existing channel or wallet,
114
+ while others require creating or joining a channel first.
115
+ - When the user asks for a transfer, first determine whether the sender has minted notes available. If not, guide them
116
+ through joining or recovering the channel wallet, funding the bridge for channel liquidity, depositing into the channel, and minting notes.
117
+ - When generating commands, use placeholders for secrets and explicit values for public fields. Show one command at a
118
+ time unless the user asks for a batch.
119
+
120
+ ## Suggested Interaction Flow
121
+
122
+ 1. Identify the target network, usually `sepolia` for testing.
123
+ 2. Identify whether a channel already exists.
124
+ 3. Identify the sender and recipient wallets or local account names.
125
+ 4. Run `help doctor`.
126
+ 5. Run `wallet list` and relevant metadata or balance checks.
127
+ 6. If needed, guide the user through `channel create`, `channel join`, `account deposit-bridge`, `wallet deposit-channel`, and
128
+ `wallet mint-notes`.
129
+ 7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
130
+ `wallet get-meta`, then build `wallet transfer-notes` with JSON arrays for `--note-ids`, `--recipients`, and `--amounts`.
131
+ 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`.
132
+
133
+ ## Example Onboarding Explanation For `channel join`
134
+
135
+ > First we need two different local secrets. Your L1 private key proves which Ethereum account pays gas and signs
136
+ > bridge transactions. We import it once into a local account nickname, so later commands can say `--account alice`
137
+ > instead of handling the raw key again. Separately, the wallet secret source derives the channel-bound spending key
138
+ > during `channel join`. It is not sent on-chain, it is not the same as your L1 private key, and the CLI does not store
139
+ > it in the wallet workspace. A wallet backup restores encrypted tracking state; the viewing key restores note
140
+ > readability; the spending key restores note spendability.
141
+
142
+ ## Example Style
143
+
144
+ If the user says, "ADDR6 sends 10 tokens privately to ADDR8", do not assume the required note exists.
145
+ First ask or check which channel and network to use, whether ADDR6 and ADDR8 are already joined, what the local wallet
146
+ names are, and whether ADDR6 has an unused note worth exactly 10 or notes that sum to 10. Then provide the next concrete
147
+ command.
@@ -210,7 +210,7 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
210
210
  fromGenesis: {
211
211
  label: "Scan From Genesis",
212
212
  type: "checkbox",
213
- hint: "Requires --source rpc. Ignore the local recovery index and replay channel logs from genesis.",
213
+ hint: "Requires --source rpc. Use only when no compatible workspace mirror is available and a full genesis replay is intentional.",
214
214
  option: "--from-genesis",
215
215
  optional: true,
216
216
  },
@@ -233,7 +233,7 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
233
233
  type: "select",
234
234
  options: ["rpc", "mirror"],
235
235
  valueLabel: "<rpc|mirror>",
236
- hint: "Optional. Defaults to rpc. mirror validates the channel leader's checkpoint manifest and downloads only the needed checkpoint or delta bundle before RPC delta replay.",
236
+ hint: "Use mirror first when a channel workspace mirror is registered. If omitted, the CLI uses rpc and resumes from the local recovery index when available.",
237
237
  option: "--source",
238
238
  optional: true,
239
239
  },
@@ -429,12 +429,13 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
429
429
  optionalFields: ["source", "fromGenesis", "outputRaw", "publishWorkspaceMirror", "leaderAccount", "output", "force"],
430
430
  usage: "--channel-name, --network, optional --source, optional --from-genesis, optional --output-raw, optional --publish-workspace-mirror with --leader-account and --output, optional --force",
431
431
  help: [
432
- "By default, --source rpc resumes RPC log scanning from the workspace recovery index when available",
432
+ "When a channel workspace mirror is registered, try --source mirror before an explicit RPC genesis rebuild",
433
433
  "--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",
434
+ "If --source is omitted, the CLI uses rpc and resumes RPC log scanning from the workspace recovery index when available",
434
435
  "RPC recovery writes a usable channel workspace checkpoint after each RPC log chunk, so interrupted runs can resume from the last completed chunk",
435
436
  "Mirror recovery uses a matching delta bundle when available; otherwise a newer verified full checkpoint replaces the local checkpoint before RPC catch-up",
436
437
  "Fails instead of falling back to genesis when no usable recovery index exists",
437
- "Use --source rpc --from-genesis to ignore the recovery index and replay logs from channel genesis",
438
+ "Use --source rpc --from-genesis only when no compatible workspace mirror is available and you intentionally want to replay logs from channel genesis",
438
439
  "--output-raw with --source rpc appends raw JSON-RPC request and response history to method-specific JSON files under the channel workspace rpcCallHistory directory; eth_getLogs is split by event",
439
440
  "--from-genesis moves the existing local channel workspace to workspace-rebuild-backups before writing the current-format workspace; local secrets are preserved",
440
441
  "--publish-workspace-mirror writes manifest.json, checkpoint.zip, and any needed delta bundle after recovery",
@@ -524,7 +525,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
524
525
  usage: "--channel-name, --network, --account, --wallet-secret-path, --acknowledge-action-impact",
525
526
  help: [
526
527
  "Refreshes the local channel workspace through the saved recovery index before joining when the scan fits the 7,200-block pre-command budget",
527
- "Fails instead of replaying from genesis; run channel recover-workspace --source rpc --from-genesis when a genesis rebuild is required",
528
+ "Fails instead of replaying from genesis; recover from a registered workspace mirror first, and use channel recover-workspace --source rpc --from-genesis only when no compatible mirror is available",
528
529
  "--wallet-secret-path is read once for channel-bound L2 spending-key derivation and is not stored in the wallet workspace",
529
530
  "Pays any join toll directly from the L1 wallet, not from bridge-deposited balance",
530
531
  "Prints the immutable policy snapshot before first registration",
package/lib/runtime.mjs CHANGED
@@ -165,6 +165,7 @@ const CLI_ERROR_CODES = Object.freeze({
165
165
  MISSING_DEPLOYMENT_ARTIFACTS: "MISSING_DEPLOYMENT_ARTIFACTS",
166
166
  MISSING_CHANNEL_REGISTRATION: "MISSING_CHANNEL_REGISTRATION",
167
167
  STALE_WORKSPACE: "STALE_WORKSPACE",
168
+ STALE_CHANNEL_ROOT: "STALE_CHANNEL_ROOT",
168
169
  });
169
170
 
170
171
  class PrivateStateCliError extends Error {
@@ -2060,7 +2061,8 @@ async function syncChannelWorkspace({
2060
2061
  throw new Error([
2061
2062
  `Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
2062
2063
  "The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
2063
- "Run channel recover-workspace first to refresh the local channel workspace.",
2064
+ `If a workspace mirror is registered, run channel recover-workspace --channel-name ${channelName} --network ${networkNameFromChainId(network.chainId)} --source mirror first.`,
2065
+ `Use channel recover-workspace --channel-name ${channelName} --network ${networkNameFromChainId(network.chainId)} --source rpc --from-genesis only when no compatible mirror is available.`,
2064
2066
  ].join(" "));
2065
2067
  }
2066
2068
  const workspaceBase = {
@@ -3872,12 +3874,14 @@ async function inspectGuideChannel({ channelName, network, provider, artifactsIn
3872
3874
  bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
3873
3875
  provider,
3874
3876
  );
3875
- const [joinToll, refundSchedule] = await Promise.all([
3877
+ const [joinToll, refundSchedule, workspaceMirror] = await Promise.all([
3876
3878
  channelManager.joinToll(),
3877
3879
  readChannelRefundSchedule(channelManager),
3880
+ readChannelWorkspaceMirror({ bridgeCore, channelId }),
3878
3881
  ]);
3879
3882
  result.onchain.joinTollBaseUnits = joinToll.toString();
3880
3883
  result.onchain.refundSchedule = refundSchedule;
3884
+ result.onchain.workspaceMirror = workspaceMirror;
3881
3885
  }
3882
3886
  } catch (error) {
3883
3887
  result.error = error.message;
@@ -4053,9 +4057,19 @@ function applyGuideNextAction(guide) {
4053
4057
  return;
4054
4058
  }
4055
4059
  if (guide.selectors.channelName && guide.state.channel?.onchain?.exists && !guide.state.channel?.local?.workspaceExists) {
4060
+ const workspaceMirror = typeof guide.state.channel.onchain.workspaceMirror === "string"
4061
+ ? guide.state.channel.onchain.workspaceMirror.trim()
4062
+ : "";
4063
+ if (workspaceMirror) {
4064
+ setGuideNextAction(guide, {
4065
+ command: `channel recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --source mirror`,
4066
+ why: "The channel has a registered workspace mirror. Use mirror recovery before considering an explicit RPC genesis rebuild.",
4067
+ });
4068
+ return;
4069
+ }
4056
4070
  setGuideNextAction(guide, {
4057
4071
  command: `channel recover-workspace --channel-name ${guide.selectors.channelName} --network ${guide.selectors.network} --source rpc --from-genesis`,
4058
- why: "The channel exists on-chain, but the local channel workspace has not been recovered yet, so there is no local recovery index to resume from.",
4072
+ why: "The channel exists on-chain, but the local channel workspace has not been recovered yet and no workspace mirror is registered. RPC genesis rebuild is the remaining explicit bootstrap path.",
4059
4073
  });
4060
4074
  return;
4061
4075
  }
@@ -4855,9 +4869,16 @@ async function handleGrothVaultMove({ args, provider, direction }) {
4855
4869
  const methodName = direction === "deposit" ? "depositToChannelVault" : "withdrawFromChannelVault";
4856
4870
  await assertWorkspaceAlignedWithChain(context);
4857
4871
  emitProgress(operationName, "submitting");
4858
- const receipt = await waitForReceipt(
4859
- await bridgeTokenVault[methodName](ethers.toBigInt(context.workspace.channelId), transition.proof, transition.update),
4860
- );
4872
+ const receipt = await submitProofBackedRootUpdate({
4873
+ context,
4874
+ walletName: walletContext.walletName,
4875
+ operationName,
4876
+ submit: () => bridgeTokenVault[methodName](
4877
+ ethers.toBigInt(context.workspace.channelId),
4878
+ transition.proof,
4879
+ transition.update,
4880
+ ),
4881
+ });
4861
4882
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
4862
4883
  expect(
4863
4884
  onchainRootVectorHash === normalizeBytes32Hex(hashRootVector(transition.nextSnapshot.stateRoots)),
@@ -7699,10 +7720,12 @@ async function executeWalletTemplateSend({
7699
7720
 
7700
7721
  await assertWorkspaceAlignedWithChain(context);
7701
7722
  emitProgress(operationName, "submitting");
7702
- const receipt =
7703
- await waitForReceipt(
7704
- await context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
7705
- );
7723
+ const receipt = await submitProofBackedRootUpdate({
7724
+ context,
7725
+ walletName: wallet.walletName,
7726
+ operationName,
7727
+ submit: () => context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
7728
+ });
7706
7729
  await waitForProviderBlockAtLeast(provider, receipt.blockNumber, { action: operationName });
7707
7730
 
7708
7731
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
@@ -8369,6 +8392,56 @@ function isContractError(error, contractInterface, errorName) {
8369
8392
  return false;
8370
8393
  }
8371
8394
 
8395
+ function isUnexpectedCurrentRootVectorError(error, context) {
8396
+ if (isContractError(error, context.channelManager.interface, "UnexpectedCurrentRootVector")) {
8397
+ return true;
8398
+ }
8399
+ return String(error?.message ?? error).includes("UnexpectedCurrentRootVector");
8400
+ }
8401
+
8402
+ async function submitProofBackedRootUpdate({
8403
+ context,
8404
+ walletName,
8405
+ operationName,
8406
+ submit,
8407
+ }) {
8408
+ try {
8409
+ return await waitForReceipt(await submit());
8410
+ } catch (error) {
8411
+ if (isUnexpectedCurrentRootVectorError(error, context)) {
8412
+ throw staleChannelRootError({
8413
+ cause: error,
8414
+ context,
8415
+ walletName,
8416
+ operationName,
8417
+ });
8418
+ }
8419
+ throw error;
8420
+ }
8421
+ }
8422
+
8423
+ function staleChannelRootError({
8424
+ cause,
8425
+ context,
8426
+ walletName,
8427
+ operationName,
8428
+ }) {
8429
+ const message = [
8430
+ `${operationName} failed because the submitted proof was generated for an older channel root.`,
8431
+ "The rejected proof cannot be reused.",
8432
+ "Do not change recipients, amounts, note counts, function arity, or split the command as recovery.",
8433
+ "Refresh the channel workspace, re-check affected wallet state when the command uses notes, then rerun the original intended command so the CLI regenerates a proof from a fresh snapshot.",
8434
+ ].join(" ");
8435
+ const error = cliError(CLI_ERROR_CODES.STALE_CHANNEL_ROOT, message, { cause });
8436
+ error.channelName = context.workspace.channelName;
8437
+ error.networkName = context.workspace.network;
8438
+ error.walletName = walletName;
8439
+ error.retryPolicy = "recover_workspace_then_regenerate_proof";
8440
+ error.semanticMutationAllowed = false;
8441
+ error.reuseProofAllowed = false;
8442
+ return error;
8443
+ }
8444
+
8372
8445
  function extractContractErrorDataCandidates(error) {
8373
8446
  return [
8374
8447
  error?.data,
@@ -11690,16 +11763,16 @@ function buildRecoveryHints(error, args = {}) {
11690
11763
  const hints = [];
11691
11764
  const networkName = typeof args.network === "string" && args.network.length > 0
11692
11765
  ? args.network
11693
- : "<NETWORK>";
11766
+ : error?.networkName ?? "<NETWORK>";
11694
11767
  const channelName = typeof args.channelName === "string" && args.channelName.length > 0
11695
11768
  ? args.channelName
11696
- : "<CHANNEL>";
11769
+ : error?.channelName ?? "<CHANNEL>";
11697
11770
  const accountName = typeof args.account === "string" && args.account.length > 0
11698
11771
  ? args.account
11699
11772
  : "<ACCOUNT>";
11700
11773
  const walletName = typeof args.wallet === "string" && args.wallet.length > 0
11701
11774
  ? args.wallet
11702
- : extractUnknownWalletName(message) ?? "<WALLET>";
11775
+ : error?.walletName ?? extractUnknownWalletName(message) ?? "<WALLET>";
11703
11776
 
11704
11777
  if (
11705
11778
  error?.code === CLI_ERROR_CODES.MISSING_RPC_URL
@@ -11742,12 +11815,29 @@ function buildRecoveryHints(error, args = {}) {
11742
11815
  }
11743
11816
 
11744
11817
  if (error?.code === CLI_ERROR_CODES.STALE_WORKSPACE) {
11745
- hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
11818
+ hints.push(`private-state-cli channel get-meta --channel-name ${channelName} --network ${networkName}`);
11819
+ hints.push(`if workspaceMirror is set: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source mirror`);
11820
+ hints.push(`otherwise use indexed RPC recovery: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
11746
11821
  hints.push(`private-state-cli help guide --network ${networkName} --channel-name ${channelName}`);
11747
11822
  }
11748
11823
 
11824
+ if (
11825
+ error?.code === CLI_ERROR_CODES.STALE_CHANNEL_ROOT
11826
+ || message.includes("UnexpectedCurrentRootVector")
11827
+ ) {
11828
+ hints.push(`private-state-cli channel get-meta --channel-name ${channelName} --network ${networkName}`);
11829
+ hints.push(`if workspaceMirror is set: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source mirror`);
11830
+ hints.push(`otherwise use indexed RPC recovery: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
11831
+ if (walletName !== "<WALLET>") {
11832
+ hints.push(`private-state-cli wallet get-notes --wallet ${walletName} --network ${networkName}`);
11833
+ }
11834
+ hints.push("rerun the original proof-backed command unchanged so the CLI regenerates a fresh proof");
11835
+ }
11836
+
11749
11837
  if (message.includes("Workspace recovery index is missing or unusable")) {
11750
- hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
11838
+ hints.push(`private-state-cli channel get-meta --channel-name ${channelName} --network ${networkName}`);
11839
+ hints.push(`if workspaceMirror is set: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source mirror`);
11840
+ hints.push(`only if no compatible workspace mirror is available: private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName} --source rpc --from-genesis`);
11751
11841
  }
11752
11842
 
11753
11843
  if (message.includes("Wallet note recovery index is missing or unusable")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "2.4.0",
3
+ "version": "2.4.2",
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",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "README.md",
29
+ "agents.md",
29
30
  "CHANGELOG.md",
30
31
  "LICENSE",
31
32
  "private-state-bridge-cli.mjs",