@tokamak-private-dapps/private-state-cli 2.1.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 +16 -0
- package/README.md +33 -12
- package/lib/private-state-cli-command-registry.mjs +9 -5
- package/lib/private-state-note-delivery.mjs +7 -14
- package/lib/private-state-runtime-management.mjs +0 -1
- package/package.json +2 -2
- package/private-state-bridge-cli.mjs +397 -181
- package/lib/private-state-tokamak-helpers.mjs +0 -184
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
5
21
|
## 2.1.0 - 2026-05-14
|
|
6
22
|
|
|
7
23
|
- Required current epoch-aware wallet workspaces for wallet commands and backup imports. Local
|
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
|
|
155
|
-
does not contain a usable index, `channel recover-workspace`
|
|
156
|
-
silently replaying logs from channel genesis. Use
|
|
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,18 @@ 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
|
|
173
|
-
`wallet get-notes`, refresh stale local workspaces through saved recovery
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|
|
178
193
|
|
|
179
194
|
Local wallet workspaces are epoch-aware. Each successful channel registration creates a wallet epoch under the
|
|
180
195
|
canonical wallet directory. `channel exit` does not delete the local wallet workspace; it marks the active epoch as
|
|
@@ -419,6 +434,12 @@ Operating rules:
|
|
|
419
434
|
- A workspace recovery index is the saved block pointer and state-root hash that lets the CLI resume log scanning
|
|
420
435
|
without replaying the channel from its creation block. If it is missing, explain `--from-genesis` before using it
|
|
421
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.
|
|
422
443
|
- When the user does not have a network RPC URL yet, explain that they need an Ethereum JSON-RPC endpoint for the
|
|
423
444
|
selected network. They can obtain one from an infrastructure provider such as Alchemy, Infura, QuickNode, or from
|
|
424
445
|
their own node. Ask the user to create or select the endpoint in that provider's UI, then paste only the endpoint URL
|
|
@@ -481,7 +502,7 @@ Suggested interaction flow:
|
|
|
481
502
|
`wallet mint-notes`.
|
|
482
503
|
7. For a confidential note transfer, select available note IDs from `wallet get-notes`, find the recipient L2 address from
|
|
483
504
|
`wallet get-meta`, then build `wallet transfer-notes`.
|
|
484
|
-
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
|
|
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`.
|
|
485
506
|
|
|
486
507
|
Example onboarding explanation for `channel join`:
|
|
487
508
|
|
|
@@ -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
|
-
"
|
|
414
|
-
"Fails
|
|
415
|
-
"Use --from-genesis to
|
|
416
|
-
"
|
|
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
|
{
|
|
@@ -619,7 +623,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
619
623
|
help: [
|
|
620
624
|
"Refreshes the local channel workspace through the saved recovery index before reading notes when the scan fits the 10 second pre-command budget",
|
|
621
625
|
"Refreshes received-note logs through the saved wallet note recovery index when the scan fits the 10 second pre-command budget",
|
|
622
|
-
"Fails instead of replaying from genesis; run wallet recover-workspace
|
|
626
|
+
"Fails instead of replaying from genesis; run wallet recover-workspace first when explicit wallet recovery is required",
|
|
623
627
|
"Use --export-evidence <PATH> with --acknowledge-full-note-plaintext-export to write a local full-note evidence ZIP for private-state-cli investigator",
|
|
624
628
|
"Evidence export includes all local epochs for the selected wallet, including exited epochs retained for dispute evidence",
|
|
625
629
|
],
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { AbiCoder, ethers } from "ethers";
|
|
3
|
-
import { deriveL2KeysFromSignature
|
|
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
|
-
|
|
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.1.
|
|
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.
|
|
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",
|
|
@@ -44,6 +44,18 @@ import {
|
|
|
44
44
|
requireCanonicalCompatibleBackendVersion,
|
|
45
45
|
requireExactSemverVersion,
|
|
46
46
|
} from "@tokamak-private-dapps/common-library/proof-backend-versioning";
|
|
47
|
+
import {
|
|
48
|
+
bigintToHex32,
|
|
49
|
+
buildStateManager,
|
|
50
|
+
buildTokamakTxSnapshot,
|
|
51
|
+
currentStorageBigInt,
|
|
52
|
+
deriveChannelTokenVaultLeafIndex,
|
|
53
|
+
deriveLiquidBalanceStorageKey,
|
|
54
|
+
fetchContractCodes,
|
|
55
|
+
normalizeBytesHex,
|
|
56
|
+
normalizeBytes32Hex,
|
|
57
|
+
serializeBigInts,
|
|
58
|
+
} from "@tokamak-private-dapps/common-library/tokamak-l2-helpers";
|
|
47
59
|
import {
|
|
48
60
|
resolveTokamakBlockInputConfig,
|
|
49
61
|
} from "@tokamak-private-dapps/common-library/tokamak-runtime-paths";
|
|
@@ -107,20 +119,6 @@ import {
|
|
|
107
119
|
normalizeEncryptedNoteValueWords,
|
|
108
120
|
unpackEncryptedNoteValue,
|
|
109
121
|
} from "./lib/private-state-note-delivery.mjs";
|
|
110
|
-
import {
|
|
111
|
-
bigintToHex32,
|
|
112
|
-
buildStateManager,
|
|
113
|
-
buildTokamakTxSnapshot,
|
|
114
|
-
bytes32FromHex,
|
|
115
|
-
currentStorageBigInt,
|
|
116
|
-
deriveChannelTokenVaultLeafIndex,
|
|
117
|
-
deriveLiquidBalanceStorageKey,
|
|
118
|
-
fetchContractCodes,
|
|
119
|
-
normalizeBytesHex,
|
|
120
|
-
normalizeBytes32Hex,
|
|
121
|
-
serializeBigInts,
|
|
122
|
-
} from "./lib/private-state-tokamak-helpers.mjs";
|
|
123
|
-
|
|
124
122
|
const require = createRequire(import.meta.url);
|
|
125
123
|
const defaultCommandCwd = process.cwd();
|
|
126
124
|
const privateStateCliPackageJson = require("./package.json");
|
|
@@ -920,7 +918,12 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
920
918
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
921
919
|
const recoverySource = resolveWorkspaceRecoverySource(args);
|
|
922
920
|
|
|
923
|
-
const {
|
|
921
|
+
const {
|
|
922
|
+
workspaceDir,
|
|
923
|
+
workspace,
|
|
924
|
+
currentSnapshot,
|
|
925
|
+
cleanRebuildBackup,
|
|
926
|
+
} = await syncChannelWorkspace({
|
|
924
927
|
workspaceName,
|
|
925
928
|
channelName,
|
|
926
929
|
network,
|
|
@@ -939,6 +942,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
939
942
|
source: workspace.recoverySource ?? recoverySource,
|
|
940
943
|
workspace: workspaceName,
|
|
941
944
|
workspaceDir,
|
|
945
|
+
cleanRebuildBackup: cleanRebuildBackup ?? null,
|
|
942
946
|
channelName,
|
|
943
947
|
channelId: workspace.channelId,
|
|
944
948
|
channelManager: workspace.channelManager,
|
|
@@ -1649,10 +1653,18 @@ async function fetchChannelWorkspaceMirror({
|
|
|
1649
1653
|
|
|
1650
1654
|
const mirrorAheadOfLocal = localRecoveryIndex
|
|
1651
1655
|
&& Number(manifestPrecheck.recoveryLastScannedBlock) > Number(localRecoveryIndex.nextBlock);
|
|
1652
|
-
const
|
|
1656
|
+
const deltaBundleDescriptor = mirrorAheadOfLocal
|
|
1657
|
+
? selectWorkspaceMirrorDeltaBundle({
|
|
1658
|
+
manifest,
|
|
1659
|
+
fromBlock: Number(localRecoveryIndex.nextBlock),
|
|
1660
|
+
toBlock: Number(manifest.checkpoint.recoveryLastScannedBlock) - 1,
|
|
1661
|
+
})
|
|
1662
|
+
: null;
|
|
1663
|
+
const bundleResult = mirrorAheadOfLocal && deltaBundleDescriptor
|
|
1653
1664
|
? await fetchAndApplyWorkspaceMirrorDelta({
|
|
1654
1665
|
manifest,
|
|
1655
1666
|
manifestUrl,
|
|
1667
|
+
bundleDescriptor: deltaBundleDescriptor,
|
|
1656
1668
|
localRecoveryIndex,
|
|
1657
1669
|
chainId,
|
|
1658
1670
|
channelId,
|
|
@@ -2054,6 +2066,7 @@ function validateWorkspaceMirrorCheckpointArchive({
|
|
|
2054
2066
|
async function fetchAndApplyWorkspaceMirrorDelta({
|
|
2055
2067
|
manifest,
|
|
2056
2068
|
manifestUrl,
|
|
2069
|
+
bundleDescriptor,
|
|
2057
2070
|
localRecoveryIndex,
|
|
2058
2071
|
chainId,
|
|
2059
2072
|
channelId,
|
|
@@ -2067,7 +2080,6 @@ async function fetchAndApplyWorkspaceMirrorDelta({
|
|
|
2067
2080
|
}) {
|
|
2068
2081
|
const fromBlock = Number(localRecoveryIndex.nextBlock);
|
|
2069
2082
|
const toBlock = Number(manifest.checkpoint.recoveryLastScannedBlock) - 1;
|
|
2070
|
-
const bundleDescriptor = selectWorkspaceMirrorDeltaBundle({ manifest, fromBlock, toBlock });
|
|
2071
2083
|
expect(
|
|
2072
2084
|
bundleDescriptor,
|
|
2073
2085
|
`Workspace mirror does not provide a delta bundle for local recovery index ${fromBlock} to checkpoint block ${toBlock}.`,
|
|
@@ -2165,6 +2177,7 @@ async function syncChannelWorkspace({
|
|
|
2165
2177
|
}) {
|
|
2166
2178
|
const workspaceDir = channelWorkspacePath(networkNameFromChainId(network.chainId), workspaceName);
|
|
2167
2179
|
const channelDir = channelDataPath(workspaceDir);
|
|
2180
|
+
let cleanRebuildBackup = null;
|
|
2168
2181
|
const hasPersistedChannelData = fs.existsSync(channelWorkspaceConfigPath(workspaceDir))
|
|
2169
2182
|
|| fs.existsSync(channelWorkspaceCurrentPath(workspaceDir))
|
|
2170
2183
|
|| fs.existsSync(channelWorkspaceOperationsPath(workspaceDir));
|
|
@@ -2176,6 +2189,13 @@ async function syncChannelWorkspace({
|
|
|
2176
2189
|
const existingArtifacts = persist && hasPersistedChannelData
|
|
2177
2190
|
? loadExistingWorkspaceArtifacts(workspaceDir)
|
|
2178
2191
|
: null;
|
|
2192
|
+
if (persist && useWorkspaceRecoveryIndex && fromGenesis) {
|
|
2193
|
+
cleanRebuildBackup = backupWorkspaceForCleanRebuild({
|
|
2194
|
+
workspaceDir,
|
|
2195
|
+
networkName: networkNameFromChainId(network.chainId),
|
|
2196
|
+
channelName,
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2179
2199
|
|
|
2180
2200
|
const { bridgeDeployment, bridgeAbiManifest } = bridgeResources;
|
|
2181
2201
|
const bridgeCore = new Contract(bridgeDeployment.bridgeCore, bridgeAbiManifest.contracts.bridgeCore.abi, provider);
|
|
@@ -2287,9 +2307,57 @@ async function syncChannelWorkspace({
|
|
|
2287
2307
|
throw new Error([
|
|
2288
2308
|
`Workspace recovery index is missing or unusable for channel ${channelName} on ${networkNameFromChainId(network.chainId)}.`,
|
|
2289
2309
|
"The CLI will not fall back to replaying channel logs from genesis unless explicitly requested.",
|
|
2290
|
-
"Run channel recover-workspace
|
|
2310
|
+
"Run channel recover-workspace first to refresh the local channel workspace.",
|
|
2291
2311
|
].join(" "));
|
|
2292
2312
|
}
|
|
2313
|
+
const workspaceBase = {
|
|
2314
|
+
name: workspaceName,
|
|
2315
|
+
network: networkNameFromChainId(network.chainId),
|
|
2316
|
+
chainId: network.chainId,
|
|
2317
|
+
appDeploymentPath: deploymentManifestPath,
|
|
2318
|
+
storageLayoutPath: storageLayoutManifestPath,
|
|
2319
|
+
channelId: channelId.toString(),
|
|
2320
|
+
channelName,
|
|
2321
|
+
dappId: Number(channelInfo.dappId),
|
|
2322
|
+
genesisBlockNumber,
|
|
2323
|
+
bridgeCore: getAddress(bridgeDeployment.bridgeCore),
|
|
2324
|
+
channelManager: getAddress(channelInfo.manager),
|
|
2325
|
+
bridgeTokenVault: getAddress(channelInfo.bridgeTokenVault),
|
|
2326
|
+
canonicalAsset,
|
|
2327
|
+
canonicalAssetDecimals,
|
|
2328
|
+
controller: controllerAddress,
|
|
2329
|
+
l2AccountingVault: l2AccountingVaultAddress,
|
|
2330
|
+
aPubBlockHash: normalizeBytes32Hex(channelInfo.aPubBlockHash),
|
|
2331
|
+
dappMetadataDigestSchema: policySnapshot.dappMetadataDigestSchema,
|
|
2332
|
+
dappMetadataDigest: policySnapshot.dappMetadataDigest,
|
|
2333
|
+
functionRoot: policySnapshot.functionRoot,
|
|
2334
|
+
policySnapshot,
|
|
2335
|
+
managedStorageAddresses,
|
|
2336
|
+
liquidBalancesSlot: liquidBalancesSlot.toString(),
|
|
2337
|
+
recoverySource,
|
|
2338
|
+
workspaceMirror: mirrorRecovery.workspaceMirror,
|
|
2339
|
+
};
|
|
2340
|
+
const buildWorkspaceForSnapshot = ({ currentSnapshot, scanRange }) => {
|
|
2341
|
+
const recoveryRootVectorHash = normalizeBytes32Hex(hashRootVector(currentSnapshot.stateRoots));
|
|
2342
|
+
return {
|
|
2343
|
+
...workspaceBase,
|
|
2344
|
+
recoveryLastScannedBlock: Number(scanRange.toBlock) + 1,
|
|
2345
|
+
recoveryRootVectorHash,
|
|
2346
|
+
recoveryScanRange: scanRange,
|
|
2347
|
+
};
|
|
2348
|
+
};
|
|
2349
|
+
const persistWorkspaceCheckpoint = persist
|
|
2350
|
+
? ({ currentSnapshot, scanRange }) => {
|
|
2351
|
+
persistChannelWorkspaceFiles({
|
|
2352
|
+
workspaceDir,
|
|
2353
|
+
channelDir,
|
|
2354
|
+
workspace: buildWorkspaceForSnapshot({ currentSnapshot, scanRange }),
|
|
2355
|
+
currentSnapshot,
|
|
2356
|
+
blockInfo,
|
|
2357
|
+
contractCodes,
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
: null;
|
|
2293
2361
|
const reconstruction = localSnapshotReusable
|
|
2294
2362
|
? {
|
|
2295
2363
|
currentSnapshot: existingArtifacts.stateSnapshot,
|
|
@@ -2325,56 +2393,23 @@ async function syncChannelWorkspace({
|
|
|
2325
2393
|
fromBlock: selectedRecoveryIndex?.nextBlock ?? genesisBlockNumber,
|
|
2326
2394
|
toBlock: latestBlock,
|
|
2327
2395
|
progressAction,
|
|
2396
|
+
onCheckpoint: persistWorkspaceCheckpoint,
|
|
2328
2397
|
});
|
|
2329
2398
|
const currentSnapshot = reconstruction.currentSnapshot;
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
name: workspaceName,
|
|
2335
|
-
network: networkNameFromChainId(network.chainId),
|
|
2336
|
-
chainId: network.chainId,
|
|
2337
|
-
appDeploymentPath: deploymentManifestPath,
|
|
2338
|
-
storageLayoutPath: storageLayoutManifestPath,
|
|
2339
|
-
channelId: channelId.toString(),
|
|
2340
|
-
channelName,
|
|
2341
|
-
dappId: Number(channelInfo.dappId),
|
|
2342
|
-
genesisBlockNumber,
|
|
2343
|
-
bridgeCore: getAddress(bridgeDeployment.bridgeCore),
|
|
2344
|
-
channelManager: getAddress(channelInfo.manager),
|
|
2345
|
-
bridgeTokenVault: getAddress(channelInfo.bridgeTokenVault),
|
|
2346
|
-
canonicalAsset,
|
|
2347
|
-
canonicalAssetDecimals,
|
|
2348
|
-
controller: controllerAddress,
|
|
2349
|
-
l2AccountingVault: l2AccountingVaultAddress,
|
|
2350
|
-
aPubBlockHash: normalizeBytes32Hex(channelInfo.aPubBlockHash),
|
|
2351
|
-
dappMetadataDigestSchema: policySnapshot.dappMetadataDigestSchema,
|
|
2352
|
-
dappMetadataDigest: policySnapshot.dappMetadataDigest,
|
|
2353
|
-
functionRoot: policySnapshot.functionRoot,
|
|
2354
|
-
policySnapshot,
|
|
2355
|
-
managedStorageAddresses,
|
|
2356
|
-
liquidBalancesSlot: liquidBalancesSlot.toString(),
|
|
2357
|
-
recoverySource,
|
|
2358
|
-
workspaceMirror: mirrorRecovery.workspaceMirror,
|
|
2359
|
-
recoveryLastScannedBlock,
|
|
2360
|
-
recoveryRootVectorHash,
|
|
2361
|
-
recoveryScanRange: reconstruction.scanRange,
|
|
2362
|
-
};
|
|
2399
|
+
const workspace = buildWorkspaceForSnapshot({
|
|
2400
|
+
currentSnapshot,
|
|
2401
|
+
scanRange: reconstruction.scanRange,
|
|
2402
|
+
});
|
|
2363
2403
|
|
|
2364
2404
|
if (persist) {
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
writeJsonIfChanged(channelWorkspaceConfigPath(workspaceDir), workspace);
|
|
2371
|
-
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.json"), currentSnapshot);
|
|
2372
|
-
writeJsonIfChanged(
|
|
2373
|
-
path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.normalized.json"),
|
|
2405
|
+
persistChannelWorkspaceFiles({
|
|
2406
|
+
workspaceDir,
|
|
2407
|
+
channelDir,
|
|
2408
|
+
workspace,
|
|
2374
2409
|
currentSnapshot,
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2410
|
+
blockInfo,
|
|
2411
|
+
contractCodes,
|
|
2412
|
+
});
|
|
2378
2413
|
}
|
|
2379
2414
|
|
|
2380
2415
|
return {
|
|
@@ -2383,6 +2418,7 @@ async function syncChannelWorkspace({
|
|
|
2383
2418
|
currentSnapshot,
|
|
2384
2419
|
blockInfo,
|
|
2385
2420
|
contractCodes,
|
|
2421
|
+
cleanRebuildBackup,
|
|
2386
2422
|
};
|
|
2387
2423
|
}
|
|
2388
2424
|
|
|
@@ -2463,39 +2499,13 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2463
2499
|
const channelName = requireArg(args.channelName, "--channel-name");
|
|
2464
2500
|
const signer = requireL1Signer(args, provider);
|
|
2465
2501
|
const walletName = walletNameForChannelAndAddress(channelName, signer.address);
|
|
2466
|
-
const
|
|
2467
|
-
const initialized = await syncChannelWorkspace({
|
|
2468
|
-
workspaceName: channelName,
|
|
2502
|
+
const channelContextResult = await loadFreshChannelWorkspaceContextResult({
|
|
2469
2503
|
channelName,
|
|
2470
|
-
|
|
2504
|
+
networkName: requireNetworkName(args),
|
|
2471
2505
|
provider,
|
|
2472
|
-
bridgeResources,
|
|
2473
|
-
persist: true,
|
|
2474
|
-
allowExistingWorkspaceSync: true,
|
|
2475
|
-
useWorkspaceRecoveryIndex: true,
|
|
2476
|
-
fromGenesis: args.fromGenesis === true,
|
|
2477
2506
|
progressAction: "wallet recover-workspace",
|
|
2478
2507
|
});
|
|
2479
|
-
const context =
|
|
2480
|
-
workspaceName: channelName,
|
|
2481
|
-
workspaceDir: initialized.workspaceDir,
|
|
2482
|
-
persistChannelWorkspace: true,
|
|
2483
|
-
workspace: initialized.workspace,
|
|
2484
|
-
bridgeAbiManifest: bridgeResources.bridgeAbiManifest,
|
|
2485
|
-
currentSnapshot: initialized.currentSnapshot,
|
|
2486
|
-
blockInfo: initialized.blockInfo,
|
|
2487
|
-
contractCodes: initialized.contractCodes,
|
|
2488
|
-
channelManager: new Contract(
|
|
2489
|
-
initialized.workspace.channelManager,
|
|
2490
|
-
bridgeResources.bridgeAbiManifest.contracts.channelManager.abi,
|
|
2491
|
-
provider,
|
|
2492
|
-
),
|
|
2493
|
-
bridgeTokenVault: new Contract(
|
|
2494
|
-
initialized.workspace.bridgeTokenVault,
|
|
2495
|
-
bridgeResources.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
2496
|
-
provider,
|
|
2497
|
-
),
|
|
2498
|
-
};
|
|
2508
|
+
const context = channelContextResult.context;
|
|
2499
2509
|
const noteReceiveKeyMaterial = await deriveNoteReceiveKeyMaterial({
|
|
2500
2510
|
signer,
|
|
2501
2511
|
chainId: network.chainId,
|
|
@@ -2563,6 +2573,8 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2563
2573
|
status: "already-recovered",
|
|
2564
2574
|
wallet: walletName,
|
|
2565
2575
|
walletDir: existingWallet.walletDir,
|
|
2576
|
+
recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
|
|
2577
|
+
channelAutoRecoveryBlockDelta: channelContextResult.autoRecoveryBlockDelta,
|
|
2566
2578
|
workspace: context.workspaceName,
|
|
2567
2579
|
channelName: context.workspace.channelName,
|
|
2568
2580
|
channelId: context.workspace.channelId,
|
|
@@ -2612,6 +2624,8 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
|
|
|
2612
2624
|
status: "recovered",
|
|
2613
2625
|
wallet: walletName,
|
|
2614
2626
|
walletDir: walletContext.walletDir,
|
|
2627
|
+
recoveredChannelWorkspace: channelContextResult.recoveredWorkspace,
|
|
2628
|
+
channelAutoRecoveryBlockDelta: channelContextResult.autoRecoveryBlockDelta,
|
|
2615
2629
|
workspace: context.workspaceName,
|
|
2616
2630
|
channelName: context.workspace.channelName,
|
|
2617
2631
|
channelId: context.workspace.channelId,
|
|
@@ -2730,6 +2744,73 @@ function uniquePaths(paths) {
|
|
|
2730
2744
|
return [...new Set(paths.map((entry) => path.resolve(entry)))];
|
|
2731
2745
|
}
|
|
2732
2746
|
|
|
2747
|
+
function backupWorkspaceForCleanRebuild({ workspaceDir, networkName, channelName }) {
|
|
2748
|
+
const resolvedWorkspaceDir = path.resolve(workspaceDir);
|
|
2749
|
+
if (!fs.existsSync(resolvedWorkspaceDir)) {
|
|
2750
|
+
return null;
|
|
2751
|
+
}
|
|
2752
|
+
expectPathWithinRoot(
|
|
2753
|
+
resolvedWorkspaceDir,
|
|
2754
|
+
workspaceRoot,
|
|
2755
|
+
`Clean rebuild refuses to move a path outside the private-state workspace root: ${resolvedWorkspaceDir}.`,
|
|
2756
|
+
);
|
|
2757
|
+
|
|
2758
|
+
const backupRoot = path.join(
|
|
2759
|
+
privateStateCliDataRoot(),
|
|
2760
|
+
"workspace-rebuild-backups",
|
|
2761
|
+
slugifyPathComponent(networkName),
|
|
2762
|
+
);
|
|
2763
|
+
ensureDir(backupRoot);
|
|
2764
|
+
const timestamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
2765
|
+
const backupBasePath = path.join(
|
|
2766
|
+
backupRoot,
|
|
2767
|
+
`${slugifyPathComponent(channelName)}-${timestamp}`,
|
|
2768
|
+
);
|
|
2769
|
+
const backupPath = nextAvailablePath(backupBasePath);
|
|
2770
|
+
fs.renameSync(resolvedWorkspaceDir, backupPath);
|
|
2771
|
+
return {
|
|
2772
|
+
workspaceDir: resolvedWorkspaceDir,
|
|
2773
|
+
backupPath,
|
|
2774
|
+
secretsPreserved: true,
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
function persistChannelWorkspaceFiles({
|
|
2779
|
+
workspaceDir,
|
|
2780
|
+
channelDir,
|
|
2781
|
+
workspace,
|
|
2782
|
+
currentSnapshot,
|
|
2783
|
+
blockInfo,
|
|
2784
|
+
contractCodes,
|
|
2785
|
+
}) {
|
|
2786
|
+
ensureDir(channelDir);
|
|
2787
|
+
ensureDir(channelWorkspaceCurrentPath(workspaceDir));
|
|
2788
|
+
ensureDir(channelWorkspaceOperationsPath(workspaceDir));
|
|
2789
|
+
ensureDir(workspaceWalletsDir(workspaceDir));
|
|
2790
|
+
|
|
2791
|
+
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.json"), currentSnapshot);
|
|
2792
|
+
writeJsonIfChanged(
|
|
2793
|
+
path.join(channelWorkspaceCurrentPath(workspaceDir), "state_snapshot.normalized.json"),
|
|
2794
|
+
currentSnapshot,
|
|
2795
|
+
);
|
|
2796
|
+
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "block_info.json"), blockInfo);
|
|
2797
|
+
writeJsonIfChanged(path.join(channelWorkspaceCurrentPath(workspaceDir), "contract_codes.json"), contractCodes);
|
|
2798
|
+
writeJsonIfChanged(channelWorkspaceConfigPath(workspaceDir), workspace);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function nextAvailablePath(basePath) {
|
|
2802
|
+
if (!fs.existsSync(basePath)) {
|
|
2803
|
+
return basePath;
|
|
2804
|
+
}
|
|
2805
|
+
for (let index = 1; index <= 1000; index += 1) {
|
|
2806
|
+
const candidate = `${basePath}-${index}`;
|
|
2807
|
+
if (!fs.existsSync(candidate)) {
|
|
2808
|
+
return candidate;
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
throw new Error(`Unable to allocate a clean rebuild backup path for ${basePath}.`);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2733
2814
|
function removeManagedRoot({ label, rootPath }) {
|
|
2734
2815
|
const resolvedPath = path.resolve(rootPath);
|
|
2735
2816
|
try {
|
|
@@ -2950,7 +3031,6 @@ function handleInvestigator() {
|
|
|
2950
3031
|
function resolveInvestigatorIndexPath() {
|
|
2951
3032
|
const candidates = [
|
|
2952
3033
|
path.join(privateStateCliPackageRoot, "investigator", "index.html"),
|
|
2953
|
-
path.resolve(privateStateCliPackageRoot, "..", "investigator", "index.html"),
|
|
2954
3034
|
];
|
|
2955
3035
|
const htmlPath = candidates.find((candidate) => fs.existsSync(candidate));
|
|
2956
3036
|
if (!htmlPath) {
|
|
@@ -6110,10 +6190,16 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
6110
6190
|
};
|
|
6111
6191
|
|
|
6112
6192
|
if (scanStartBlock > latestBlock) {
|
|
6193
|
+
const reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6194
|
+
walletContext,
|
|
6195
|
+
currentSnapshot: context.currentSnapshot,
|
|
6196
|
+
controllerAddress: context.workspace.controller,
|
|
6197
|
+
});
|
|
6113
6198
|
walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
|
|
6114
6199
|
persistWallet(walletContext);
|
|
6115
6200
|
return {
|
|
6116
6201
|
importedNotes: [],
|
|
6202
|
+
reconciledState,
|
|
6117
6203
|
scannedLogs: 0,
|
|
6118
6204
|
scanRange,
|
|
6119
6205
|
};
|
|
@@ -6124,18 +6210,67 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
6124
6210
|
);
|
|
6125
6211
|
const commitmentExistsSlot = ethers.toBigInt(findStorageSlot(storageLayoutManifest, "PrivateStateController", "commitmentExists"));
|
|
6126
6212
|
const nullifierUsedSlot = ethers.toBigInt(findStorageSlot(storageLayoutManifest, "PrivateStateController", "nullifierUsed"));
|
|
6127
|
-
const
|
|
6213
|
+
const importedNotes = [];
|
|
6214
|
+
let reconciledState = null;
|
|
6215
|
+
let scannedLogs = 0;
|
|
6216
|
+
|
|
6217
|
+
await fetchLogsChunked(provider, {
|
|
6128
6218
|
address: context.workspace.channelManager,
|
|
6129
6219
|
topics: [NOTE_VALUE_ENCRYPTED_TOPIC],
|
|
6130
6220
|
fromBlock: scanStartBlock,
|
|
6131
6221
|
toBlock: latestBlock,
|
|
6222
|
+
collectLogs: false,
|
|
6132
6223
|
onProgress: progressAction
|
|
6133
6224
|
? createRpcLogScanProgress({ action: progressAction, label: "note-delivery events" })
|
|
6134
6225
|
: null,
|
|
6226
|
+
onChunk: async ({ logs, chunkToBlock }) => {
|
|
6227
|
+
scannedLogs += logs.length;
|
|
6228
|
+
const importedCandidates = await recoverDeliveredNoteCandidatesFromLogs({
|
|
6229
|
+
logs,
|
|
6230
|
+
walletContext,
|
|
6231
|
+
context,
|
|
6232
|
+
noteReceivePrivateKey,
|
|
6233
|
+
commitmentExistsSlot,
|
|
6234
|
+
nullifierUsedSlot,
|
|
6235
|
+
});
|
|
6236
|
+
if (importedCandidates.length > 0) {
|
|
6237
|
+
importedNotes.push(...mergeTrackedNotesIntoWallet(walletContext, importedCandidates));
|
|
6238
|
+
reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6239
|
+
walletContext,
|
|
6240
|
+
currentSnapshot: context.currentSnapshot,
|
|
6241
|
+
controllerAddress: context.workspace.controller,
|
|
6242
|
+
});
|
|
6243
|
+
}
|
|
6244
|
+
walletContext.wallet.noteReceiveLastScannedBlock = Number(chunkToBlock) + 1;
|
|
6245
|
+
persistWallet(walletContext);
|
|
6246
|
+
},
|
|
6135
6247
|
});
|
|
6136
6248
|
|
|
6249
|
+
reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6250
|
+
walletContext,
|
|
6251
|
+
currentSnapshot: context.currentSnapshot,
|
|
6252
|
+
controllerAddress: context.workspace.controller,
|
|
6253
|
+
});
|
|
6254
|
+
walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
|
|
6255
|
+
persistWallet(walletContext);
|
|
6256
|
+
return {
|
|
6257
|
+
importedNotes,
|
|
6258
|
+
reconciledState,
|
|
6259
|
+
scannedLogs,
|
|
6260
|
+
scanRange,
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
6263
|
+
|
|
6264
|
+
async function recoverDeliveredNoteCandidatesFromLogs({
|
|
6265
|
+
logs,
|
|
6266
|
+
walletContext,
|
|
6267
|
+
context,
|
|
6268
|
+
noteReceivePrivateKey,
|
|
6269
|
+
commitmentExistsSlot,
|
|
6270
|
+
nullifierUsedSlot,
|
|
6271
|
+
}) {
|
|
6137
6272
|
const importedCandidates = [];
|
|
6138
|
-
for (const log of
|
|
6273
|
+
for (const log of logs) {
|
|
6139
6274
|
const encryptedNoteValue = extractEncryptedNoteValueFromBridgeLog(log);
|
|
6140
6275
|
if (!encryptedNoteValue) {
|
|
6141
6276
|
continue;
|
|
@@ -6200,21 +6335,7 @@ async function recoverDeliveredNotesFromEventLogs({
|
|
|
6200
6335
|
}
|
|
6201
6336
|
importedCandidates.push(trackedNote);
|
|
6202
6337
|
}
|
|
6203
|
-
|
|
6204
|
-
const importedNotes = mergeTrackedNotesIntoWallet(walletContext, importedCandidates);
|
|
6205
|
-
const reconciledState = await reconcileWalletNotesWithBridgeState({
|
|
6206
|
-
walletContext,
|
|
6207
|
-
currentSnapshot: context.currentSnapshot,
|
|
6208
|
-
controllerAddress: context.workspace.controller,
|
|
6209
|
-
});
|
|
6210
|
-
walletContext.wallet.noteReceiveLastScannedBlock = latestBlock + 1;
|
|
6211
|
-
persistWallet(walletContext);
|
|
6212
|
-
return {
|
|
6213
|
-
importedNotes,
|
|
6214
|
-
reconciledState,
|
|
6215
|
-
scannedLogs: observedLogs.length,
|
|
6216
|
-
scanRange,
|
|
6217
|
-
};
|
|
6338
|
+
return importedCandidates;
|
|
6218
6339
|
}
|
|
6219
6340
|
|
|
6220
6341
|
function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, latestBlock }) {
|
|
@@ -6228,7 +6349,7 @@ function requireUsableWalletNoteReceiveRecoveryIndex({ walletContext, context, l
|
|
|
6228
6349
|
throw new Error([
|
|
6229
6350
|
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
6230
6351
|
`Expected noteReceiveLastScannedBlock to be an integer between ${genesisBlockNumber} and ${Number(latestBlock) + 1}.`,
|
|
6231
|
-
"Run wallet recover-workspace --from-genesis to
|
|
6352
|
+
"Run wallet recover-workspace --from-genesis to restart received-note scanning from channel genesis.",
|
|
6232
6353
|
].join(" "));
|
|
6233
6354
|
}
|
|
6234
6355
|
return nextBlock;
|
|
@@ -6274,7 +6395,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6274
6395
|
throw new Error([
|
|
6275
6396
|
`Wallet note recovery index is missing or unusable for wallet ${walletContext.walletName}.`,
|
|
6276
6397
|
"Automatic wallet recovery uses only the saved note recovery index and will not replay from genesis.",
|
|
6277
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if
|
|
6398
|
+
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
6278
6399
|
`Details: ${indexError.message}`,
|
|
6279
6400
|
].join(" "));
|
|
6280
6401
|
}
|
|
@@ -6312,7 +6433,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6312
6433
|
throw new Error([
|
|
6313
6434
|
`Wallet workspace is not current for wallet ${walletContext.walletName}.`,
|
|
6314
6435
|
"Automatic wallet recovery uses only the saved note recovery index and will not replay from genesis.",
|
|
6315
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if
|
|
6436
|
+
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
6316
6437
|
`Details: ${recoveryError.message}`,
|
|
6317
6438
|
].join(" "));
|
|
6318
6439
|
}
|
|
@@ -6328,7 +6449,7 @@ async function ensureWalletNoteReceiveStateCurrent({
|
|
|
6328
6449
|
throw new Error([
|
|
6329
6450
|
`Wallet workspace is still stale after recovery-index sync for wallet ${walletContext.walletName}.`,
|
|
6330
6451
|
"Automatic wallet recovery will not replay from genesis.",
|
|
6331
|
-
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if
|
|
6452
|
+
`Run wallet recover-workspace --channel-name ${context.workspace.channelName} --network ${context.workspace.network} --account <ACCOUNT> --from-genesis if received-note scanning must restart from channel genesis.`,
|
|
6332
6453
|
`Details: ${postRecoveryError.message}`,
|
|
6333
6454
|
].join(" "));
|
|
6334
6455
|
}
|
|
@@ -6948,7 +7069,7 @@ async function recoverChannelWorkspaceFromIndexOnly({
|
|
|
6948
7069
|
throw new Error([
|
|
6949
7070
|
`Channel workspace is not current for ${channelName} on ${networkName}.`,
|
|
6950
7071
|
"Automatic channel workspace recovery uses only the saved recovery index and will not replay from genesis.",
|
|
6951
|
-
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
7072
|
+
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName} first.`,
|
|
6952
7073
|
`Details: ${recoveryError.message}`,
|
|
6953
7074
|
cause ? `Initial freshness failure: ${cause.message}` : null,
|
|
6954
7075
|
].filter(Boolean).join(" "));
|
|
@@ -6961,7 +7082,7 @@ async function recoverChannelWorkspaceFromIndexOnly({
|
|
|
6961
7082
|
throw new Error([
|
|
6962
7083
|
`Channel workspace is still stale after recovery-index sync for ${channelName} on ${networkName}.`,
|
|
6963
7084
|
"Automatic channel workspace recovery will not replay from genesis.",
|
|
6964
|
-
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
7085
|
+
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName} first.`,
|
|
6965
7086
|
`Details: ${postRecoveryError.message}`,
|
|
6966
7087
|
cause ? `Initial freshness failure: ${cause.message}` : null,
|
|
6967
7088
|
].filter(Boolean).join(" "));
|
|
@@ -6985,7 +7106,7 @@ async function requireChannelWorkspaceRecoveryIndexForAutoRefresh({
|
|
|
6985
7106
|
throw new Error([
|
|
6986
7107
|
message,
|
|
6987
7108
|
"Automatic channel workspace recovery uses only the saved recovery index and will not replay from genesis.",
|
|
6988
|
-
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
7109
|
+
`Run channel recover-workspace --channel-name ${channelName} --network ${networkName} first.`,
|
|
6989
7110
|
cause ? `Initial freshness failure: ${cause.message}` : null,
|
|
6990
7111
|
].filter(Boolean).join(" "));
|
|
6991
7112
|
};
|
|
@@ -7995,9 +8116,9 @@ async function buildGrothTransition({ operationDir, workspace, stateManager, vau
|
|
|
7995
8116
|
update: {
|
|
7996
8117
|
currentRootVector: normalizedRootVector(currentSnapshot.stateRoots),
|
|
7997
8118
|
updatedRoot: bigintToHex32(updatedRoot),
|
|
7998
|
-
currentUserKey:
|
|
8119
|
+
currentUserKey: normalizeBytes32Hex(keyHex),
|
|
7999
8120
|
currentUserValue: currentValue,
|
|
8000
|
-
updatedUserKey:
|
|
8121
|
+
updatedUserKey: normalizeBytes32Hex(keyHex),
|
|
8001
8122
|
updatedUserValue: nextValue,
|
|
8002
8123
|
},
|
|
8003
8124
|
nextSnapshot,
|
|
@@ -8188,10 +8309,6 @@ function normalizeBytes12Hex(value) {
|
|
|
8188
8309
|
return normalizeBytesHex(value, 12);
|
|
8189
8310
|
}
|
|
8190
8311
|
|
|
8191
|
-
function normalizeBytes16Hex(value) {
|
|
8192
|
-
return normalizeBytesHex(value, 16);
|
|
8193
|
-
}
|
|
8194
|
-
|
|
8195
8312
|
function hashTokamakPublicInputs(values) {
|
|
8196
8313
|
return keccak256(abiCoder.encode(["uint256[]"], [values]));
|
|
8197
8314
|
}
|
|
@@ -8319,6 +8436,67 @@ async function fetchChannelRecoveryLogs({
|
|
|
8319
8436
|
};
|
|
8320
8437
|
}
|
|
8321
8438
|
|
|
8439
|
+
async function fetchChannelRecoveryEventGroupsChunked({
|
|
8440
|
+
provider,
|
|
8441
|
+
bridgeAbiManifest,
|
|
8442
|
+
channelInfo,
|
|
8443
|
+
channelManager = null,
|
|
8444
|
+
fromBlock,
|
|
8445
|
+
toBlock,
|
|
8446
|
+
progressAction = null,
|
|
8447
|
+
onChunk,
|
|
8448
|
+
}) {
|
|
8449
|
+
const resolvedChannelManager = channelManager ?? new Contract(
|
|
8450
|
+
channelInfo.manager,
|
|
8451
|
+
bridgeAbiManifest.contracts.channelManager.abi,
|
|
8452
|
+
provider,
|
|
8453
|
+
);
|
|
8454
|
+
const currentRootVectorObservedTopic =
|
|
8455
|
+
normalizeBytes32Hex(resolvedChannelManager.interface.getEvent("CurrentRootVectorObserved").topicHash);
|
|
8456
|
+
const bridgeTokenVault = new Contract(
|
|
8457
|
+
channelInfo.bridgeTokenVault,
|
|
8458
|
+
bridgeAbiManifest.contracts.bridgeTokenVault.abi,
|
|
8459
|
+
provider,
|
|
8460
|
+
);
|
|
8461
|
+
const bridgeVaultTopic = bridgeTokenVault.interface.getEvent("StorageWriteObserved").topicHash;
|
|
8462
|
+
|
|
8463
|
+
await fetchLogsChunked(provider, {
|
|
8464
|
+
address: channelInfo.manager,
|
|
8465
|
+
topics: [[
|
|
8466
|
+
currentRootVectorObservedTopic,
|
|
8467
|
+
CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC,
|
|
8468
|
+
VAULT_STORAGE_WRITE_OBSERVED_TOPIC,
|
|
8469
|
+
]],
|
|
8470
|
+
fromBlock,
|
|
8471
|
+
toBlock,
|
|
8472
|
+
collectLogs: false,
|
|
8473
|
+
onProgress: progressAction
|
|
8474
|
+
? createRpcLogScanProgress({ action: progressAction, label: "channel-recovery chunks" })
|
|
8475
|
+
: null,
|
|
8476
|
+
onChunk: async ({ logs: channelManagerLogs, chunkFromBlock, chunkToBlock }) => {
|
|
8477
|
+
await throttleLogRequest();
|
|
8478
|
+
const bridgeVaultLogs = await provider.getLogs({
|
|
8479
|
+
address: channelInfo.bridgeTokenVault,
|
|
8480
|
+
topics: [bridgeVaultTopic],
|
|
8481
|
+
fromBlock: chunkFromBlock,
|
|
8482
|
+
toBlock: chunkToBlock,
|
|
8483
|
+
});
|
|
8484
|
+
const groupedValues = normalizeWorkspaceMirrorDeltaEventGroups({
|
|
8485
|
+
logs: [...channelManagerLogs, ...bridgeVaultLogs],
|
|
8486
|
+
channelInfo,
|
|
8487
|
+
bridgeAbiManifest,
|
|
8488
|
+
fromBlock: chunkFromBlock,
|
|
8489
|
+
toBlock: chunkToBlock,
|
|
8490
|
+
});
|
|
8491
|
+
await onChunk?.({
|
|
8492
|
+
groupedValues,
|
|
8493
|
+
chunkFromBlock,
|
|
8494
|
+
chunkToBlock,
|
|
8495
|
+
});
|
|
8496
|
+
},
|
|
8497
|
+
});
|
|
8498
|
+
}
|
|
8499
|
+
|
|
8322
8500
|
async function reconstructChannelSnapshot({
|
|
8323
8501
|
provider,
|
|
8324
8502
|
bridgeAbiManifest,
|
|
@@ -8336,6 +8514,7 @@ async function reconstructChannelSnapshot({
|
|
|
8336
8514
|
fromBlock = genesisBlockNumber,
|
|
8337
8515
|
toBlock = null,
|
|
8338
8516
|
progressAction = null,
|
|
8517
|
+
onCheckpoint = null,
|
|
8339
8518
|
}) {
|
|
8340
8519
|
let startingSnapshot = baseSnapshot;
|
|
8341
8520
|
if (!startingSnapshot) {
|
|
@@ -8351,11 +8530,20 @@ async function reconstructChannelSnapshot({
|
|
|
8351
8530
|
|
|
8352
8531
|
const latestBlock = toBlock === null ? await provider.getBlockNumber() : Number(toBlock);
|
|
8353
8532
|
const scanFromBlock = Math.max(Number(genesisBlockNumber), Number(fromBlock));
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8533
|
+
let currentSnapshot = startingSnapshot;
|
|
8534
|
+
if (onCheckpoint && scanFromBlock <= latestBlock) {
|
|
8535
|
+
await onCheckpoint({
|
|
8536
|
+
currentSnapshot,
|
|
8537
|
+
scanRange: {
|
|
8538
|
+
fromBlock: scanFromBlock,
|
|
8539
|
+
toBlock: scanFromBlock - 1,
|
|
8540
|
+
mode: baseSnapshot ? "recovery-index-initial" : "genesis-initial",
|
|
8541
|
+
},
|
|
8542
|
+
});
|
|
8543
|
+
}
|
|
8544
|
+
const stateManager = await buildStateManager(currentSnapshot, contractCodes);
|
|
8545
|
+
|
|
8546
|
+
await fetchChannelRecoveryEventGroupsChunked({
|
|
8359
8547
|
provider,
|
|
8360
8548
|
bridgeAbiManifest,
|
|
8361
8549
|
channelInfo,
|
|
@@ -8363,37 +8551,27 @@ async function reconstructChannelSnapshot({
|
|
|
8363
8551
|
fromBlock: scanFromBlock,
|
|
8364
8552
|
toBlock: latestBlock,
|
|
8365
8553
|
progressAction,
|
|
8366
|
-
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
};
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
const groupedValues = [...groupedEvents.values()].sort((left, right) => compareLogsByPosition(left[0], right[0]));
|
|
8389
|
-
const currentSnapshot = await applyChannelRecoveryEventGroups({
|
|
8390
|
-
startingSnapshot,
|
|
8391
|
-
groupedValues,
|
|
8392
|
-
contractCodes,
|
|
8393
|
-
channelInfo,
|
|
8394
|
-
controllerAddress,
|
|
8395
|
-
l2AccountingVaultAddress,
|
|
8396
|
-
liquidBalancesSlot,
|
|
8554
|
+
onChunk: async ({ groupedValues, chunkFromBlock, chunkToBlock }) => {
|
|
8555
|
+
currentSnapshot = await applyChannelRecoveryEventGroupsToStateManager({
|
|
8556
|
+
stateManager,
|
|
8557
|
+
fallbackSnapshot: currentSnapshot,
|
|
8558
|
+
groupedValues,
|
|
8559
|
+
channelInfo,
|
|
8560
|
+
controllerAddress,
|
|
8561
|
+
l2AccountingVaultAddress,
|
|
8562
|
+
liquidBalancesSlot,
|
|
8563
|
+
});
|
|
8564
|
+
await onCheckpoint?.({
|
|
8565
|
+
currentSnapshot,
|
|
8566
|
+
scanRange: {
|
|
8567
|
+
fromBlock: scanFromBlock,
|
|
8568
|
+
toBlock: chunkToBlock,
|
|
8569
|
+
chunkFromBlock,
|
|
8570
|
+
chunkToBlock,
|
|
8571
|
+
mode: baseSnapshot ? "recovery-index" : "genesis",
|
|
8572
|
+
},
|
|
8573
|
+
});
|
|
8574
|
+
},
|
|
8397
8575
|
});
|
|
8398
8576
|
|
|
8399
8577
|
expect(
|
|
@@ -8421,9 +8599,28 @@ async function applyChannelRecoveryEventGroups({
|
|
|
8421
8599
|
l2AccountingVaultAddress,
|
|
8422
8600
|
liquidBalancesSlot,
|
|
8423
8601
|
}) {
|
|
8424
|
-
|
|
8425
|
-
|
|
8602
|
+
const stateManager = await buildStateManager(startingSnapshot, contractCodes);
|
|
8603
|
+
return applyChannelRecoveryEventGroupsToStateManager({
|
|
8604
|
+
stateManager,
|
|
8605
|
+
fallbackSnapshot: startingSnapshot,
|
|
8606
|
+
groupedValues,
|
|
8607
|
+
channelInfo,
|
|
8608
|
+
controllerAddress,
|
|
8609
|
+
l2AccountingVaultAddress,
|
|
8610
|
+
liquidBalancesSlot,
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8426
8613
|
|
|
8614
|
+
async function applyChannelRecoveryEventGroupsToStateManager({
|
|
8615
|
+
stateManager,
|
|
8616
|
+
fallbackSnapshot,
|
|
8617
|
+
groupedValues,
|
|
8618
|
+
channelInfo,
|
|
8619
|
+
controllerAddress,
|
|
8620
|
+
l2AccountingVaultAddress,
|
|
8621
|
+
liquidBalancesSlot,
|
|
8622
|
+
}) {
|
|
8623
|
+
let currentSnapshot = fallbackSnapshot;
|
|
8427
8624
|
for (const group of groupedValues) {
|
|
8428
8625
|
const orderedGroup = [...group].sort(compareLogsByPosition);
|
|
8429
8626
|
const rootEvent = orderedGroup.find(
|
|
@@ -8670,11 +8867,14 @@ async function fetchLogsChunked(provider, {
|
|
|
8670
8867
|
fromBlock,
|
|
8671
8868
|
toBlock,
|
|
8672
8869
|
initialChunkSize = DEFAULT_LOG_CHUNK_SIZE,
|
|
8870
|
+
collectLogs = true,
|
|
8673
8871
|
onProgress = null,
|
|
8872
|
+
onChunk = null,
|
|
8674
8873
|
}) {
|
|
8675
8874
|
const normalizedFromBlock = Number(fromBlock);
|
|
8676
8875
|
const resolvedToBlock = toBlock === "latest" ? await provider.getBlockNumber() : Number(toBlock);
|
|
8677
8876
|
const aggregatedLogs = [];
|
|
8877
|
+
let logsFound = 0;
|
|
8678
8878
|
|
|
8679
8879
|
if (normalizedFromBlock > resolvedToBlock) {
|
|
8680
8880
|
onProgress?.({
|
|
@@ -8702,27 +8902,15 @@ async function fetchLogsChunked(provider, {
|
|
|
8702
8902
|
let cursor = normalizedFromBlock;
|
|
8703
8903
|
while (cursor <= resolvedToBlock) {
|
|
8704
8904
|
const chunkToBlock = Math.min(resolvedToBlock, cursor + chunkSize - 1);
|
|
8905
|
+
let logs;
|
|
8705
8906
|
try {
|
|
8706
8907
|
await throttleLogRequest();
|
|
8707
|
-
|
|
8908
|
+
logs = await provider.getLogs({
|
|
8708
8909
|
address,
|
|
8709
8910
|
topics,
|
|
8710
8911
|
fromBlock: cursor,
|
|
8711
8912
|
toBlock: chunkToBlock,
|
|
8712
8913
|
});
|
|
8713
|
-
aggregatedLogs.push(...logs);
|
|
8714
|
-
onProgress?.({
|
|
8715
|
-
status: "progress",
|
|
8716
|
-
fromBlock: normalizedFromBlock,
|
|
8717
|
-
toBlock: resolvedToBlock,
|
|
8718
|
-
chunkFromBlock: cursor,
|
|
8719
|
-
chunkToBlock,
|
|
8720
|
-
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
8721
|
-
totalBlocks,
|
|
8722
|
-
logsFound: aggregatedLogs.length,
|
|
8723
|
-
chunkLogs: logs.length,
|
|
8724
|
-
});
|
|
8725
|
-
cursor = chunkToBlock + 1;
|
|
8726
8914
|
} catch (error) {
|
|
8727
8915
|
if (isRateLimitError(error)) {
|
|
8728
8916
|
throw new Error(
|
|
@@ -8735,7 +8923,36 @@ async function fetchLogsChunked(provider, {
|
|
|
8735
8923
|
throw error;
|
|
8736
8924
|
}
|
|
8737
8925
|
chunkSize = suggestedChunkSize;
|
|
8926
|
+
continue;
|
|
8927
|
+
}
|
|
8928
|
+
logsFound += logs.length;
|
|
8929
|
+
if (collectLogs) {
|
|
8930
|
+
aggregatedLogs.push(...logs);
|
|
8738
8931
|
}
|
|
8932
|
+
onProgress?.({
|
|
8933
|
+
status: "progress",
|
|
8934
|
+
fromBlock: normalizedFromBlock,
|
|
8935
|
+
toBlock: resolvedToBlock,
|
|
8936
|
+
chunkFromBlock: cursor,
|
|
8937
|
+
chunkToBlock,
|
|
8938
|
+
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
8939
|
+
totalBlocks,
|
|
8940
|
+
logsFound,
|
|
8941
|
+
chunkLogs: logs.length,
|
|
8942
|
+
});
|
|
8943
|
+
await onChunk?.({
|
|
8944
|
+
status: "progress",
|
|
8945
|
+
fromBlock: normalizedFromBlock,
|
|
8946
|
+
toBlock: resolvedToBlock,
|
|
8947
|
+
chunkFromBlock: cursor,
|
|
8948
|
+
chunkToBlock,
|
|
8949
|
+
scannedBlocks: chunkToBlock - normalizedFromBlock + 1,
|
|
8950
|
+
totalBlocks,
|
|
8951
|
+
logsFound,
|
|
8952
|
+
chunkLogs: logs.length,
|
|
8953
|
+
logs,
|
|
8954
|
+
});
|
|
8955
|
+
cursor = chunkToBlock + 1;
|
|
8739
8956
|
}
|
|
8740
8957
|
|
|
8741
8958
|
onProgress?.({
|
|
@@ -8744,7 +8961,7 @@ async function fetchLogsChunked(provider, {
|
|
|
8744
8961
|
toBlock: resolvedToBlock,
|
|
8745
8962
|
scannedBlocks: totalBlocks,
|
|
8746
8963
|
totalBlocks,
|
|
8747
|
-
logsFound
|
|
8964
|
+
logsFound,
|
|
8748
8965
|
});
|
|
8749
8966
|
|
|
8750
8967
|
return aggregatedLogs;
|
|
@@ -8780,7 +8997,7 @@ function assertAutoRecoveryBlockBudget({
|
|
|
8780
8997
|
`Automatic recovery for ${label} would exceed the ${AUTO_RECOVERY_BLOCK_BUDGET}-block pre-command budget.`,
|
|
8781
8998
|
`Recovery delta is ${blockDelta} blocks from ${normalizedFromBlock} to ${normalizedToBlock}.`,
|
|
8782
8999
|
`Remaining automatic recovery budget is ${normalizedBudget} blocks.`,
|
|
8783
|
-
`Run ${recoveryCommand} first
|
|
9000
|
+
`Run ${recoveryCommand} first.`,
|
|
8784
9001
|
].join(" "));
|
|
8785
9002
|
}
|
|
8786
9003
|
|
|
@@ -10512,9 +10729,9 @@ function writeEncryptedWalletFile(filePath, plaintextBytes, walletSecret) {
|
|
|
10512
10729
|
version: WALLET_ENCRYPTION_VERSION,
|
|
10513
10730
|
algorithm: WALLET_ENCRYPTION_ALGORITHM,
|
|
10514
10731
|
kdf: "scrypt",
|
|
10515
|
-
salt:
|
|
10732
|
+
salt: normalizeBytesHex(salt, 16),
|
|
10516
10733
|
iv: normalizeBytes12Hex(iv),
|
|
10517
|
-
tag:
|
|
10734
|
+
tag: normalizeBytesHex(tag, 16),
|
|
10518
10735
|
ciphertext: ethers.hexlify(ciphertext),
|
|
10519
10736
|
};
|
|
10520
10737
|
fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
@@ -11012,8 +11229,7 @@ function buildRecoveryHints(error, args = {}) {
|
|
|
11012
11229
|
}
|
|
11013
11230
|
|
|
11014
11231
|
if (message.includes("Workspace recovery index is missing or unusable")) {
|
|
11015
|
-
hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}
|
|
11016
|
-
hints.push(`private-state-cli wallet recover-workspace --channel-name ${channelName} --network ${networkName} --account ${accountName} --from-genesis`);
|
|
11232
|
+
hints.push(`private-state-cli channel recover-workspace --channel-name ${channelName} --network ${networkName}`);
|
|
11017
11233
|
}
|
|
11018
11234
|
|
|
11019
11235
|
if (message.includes("Wallet note recovery index is missing or unusable")) {
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { ethers, getAddress } from "ethers";
|
|
2
|
-
import {
|
|
3
|
-
MAX_MT_LEAVES,
|
|
4
|
-
TokamakL2StateManager,
|
|
5
|
-
createTokamakL2Common,
|
|
6
|
-
createTokamakL2StateManagerFromStateSnapshot,
|
|
7
|
-
createTokamakL2Tx,
|
|
8
|
-
getUserStorageKey,
|
|
9
|
-
poseidon,
|
|
10
|
-
} from "tokamak-l2js";
|
|
11
|
-
import {
|
|
12
|
-
addHexPrefix,
|
|
13
|
-
bytesToBigInt,
|
|
14
|
-
bytesToHex,
|
|
15
|
-
createAddressFromString,
|
|
16
|
-
hexToBigInt,
|
|
17
|
-
hexToBytes,
|
|
18
|
-
} from "@ethereumjs/util";
|
|
19
|
-
import {
|
|
20
|
-
resolveTokamakBlockInputConfig,
|
|
21
|
-
} from "@tokamak-private-dapps/common-library/tokamak-runtime-paths";
|
|
22
|
-
|
|
23
|
-
const { previousBlockHashCount: tokamakPrevBlockHashCount } = resolveTokamakBlockInputConfig();
|
|
24
|
-
|
|
25
|
-
export function normalizeBytesHex(value, byteLength) {
|
|
26
|
-
if (!Number.isInteger(byteLength) || byteLength <= 0) {
|
|
27
|
-
throw new Error("normalizeBytesHex requires a positive byte length.");
|
|
28
|
-
}
|
|
29
|
-
const targetHexLength = byteLength * 2;
|
|
30
|
-
let hex;
|
|
31
|
-
if (typeof value === "string") {
|
|
32
|
-
const trimmed = value.trim();
|
|
33
|
-
if (!/^0x[0-9a-fA-F]*$/.test(trimmed)) {
|
|
34
|
-
throw new Error(`Expected a hex string, received ${value}.`);
|
|
35
|
-
}
|
|
36
|
-
hex = trimmed.replace(/^0x/i, "");
|
|
37
|
-
if (hex.length % 2 !== 0) {
|
|
38
|
-
hex = `0${hex}`;
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
hex = ethers.hexlify(value).replace(/^0x/i, "");
|
|
42
|
-
}
|
|
43
|
-
if (hex.length > targetHexLength) {
|
|
44
|
-
throw new Error(`Expected at most ${byteLength} bytes, received ${Math.ceil(hex.length / 2)} bytes.`);
|
|
45
|
-
}
|
|
46
|
-
return `0x${hex.padStart(targetHexLength, "0").toLowerCase()}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function normalizeBytes32Hex(hexValue) {
|
|
50
|
-
return normalizeBytesHex(hexValue, 32);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function bytes32FromHex(hexValue) {
|
|
54
|
-
return normalizeBytes32Hex(hexValue);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function bigintToHex32(value) {
|
|
58
|
-
return normalizeBytes32Hex(ethers.toBeHex(value));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function poseidonHexFromBytes(bytesLike) {
|
|
62
|
-
return ethers.hexlify(poseidon(ethers.getBytes(bytesLike))).toLowerCase();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function serializeBigInts(value) {
|
|
66
|
-
return JSON.parse(JSON.stringify(value, (_key, current) => (
|
|
67
|
-
typeof current === "bigint" ? current.toString() : current
|
|
68
|
-
)));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function buildTokamakTxSnapshot({ signerPrivateKey, senderPubKey, to, data, nonce }) {
|
|
72
|
-
return serializeBigInts(
|
|
73
|
-
createTokamakL2Tx(
|
|
74
|
-
{
|
|
75
|
-
nonce: ethers.toBigInt(nonce),
|
|
76
|
-
to: createAddressFromString(to),
|
|
77
|
-
data: hexToBytes(addHexPrefix(String(data ?? "").replace(/^0x/i, ""))),
|
|
78
|
-
senderPubKey,
|
|
79
|
-
},
|
|
80
|
-
{ common: createTokamakL2Common() },
|
|
81
|
-
)
|
|
82
|
-
.sign(signerPrivateKey)
|
|
83
|
-
.captureTxSnapshot(),
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function fetchContractCodes(provider, addresses, { requireBytecode = false } = {}) {
|
|
88
|
-
const codes = [];
|
|
89
|
-
for (const address of addresses) {
|
|
90
|
-
const normalizedAddress = getAddress(address);
|
|
91
|
-
const code = await provider.getCode(normalizedAddress);
|
|
92
|
-
if (requireBytecode && code === "0x") {
|
|
93
|
-
throw new Error(`No deployed bytecode found at ${normalizedAddress}.`);
|
|
94
|
-
}
|
|
95
|
-
codes.push({ address: normalizedAddress, code });
|
|
96
|
-
}
|
|
97
|
-
return codes;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export async function buildStateManager(snapshot, contractCodes) {
|
|
101
|
-
return createTokamakL2StateManagerFromStateSnapshot(snapshot, {
|
|
102
|
-
contractCodes: contractCodes.map((entry) => ({
|
|
103
|
-
address: createAddressFromString(entry.address),
|
|
104
|
-
code: addHexPrefix(entry.code),
|
|
105
|
-
})),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export async function currentStorageBigInt(stateManager, address, keyHex) {
|
|
110
|
-
const valueBytes = await stateManager.getStorage(
|
|
111
|
-
createAddressFromString(address),
|
|
112
|
-
hexToBytes(addHexPrefix(String(keyHex ?? "").replace(/^0x/i, ""))),
|
|
113
|
-
);
|
|
114
|
-
if (valueBytes.length === 0) {
|
|
115
|
-
return 0n;
|
|
116
|
-
}
|
|
117
|
-
return bytesToBigInt(valueBytes);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export async function putStorageValue(stateManager, address, keyHex, nextValue) {
|
|
121
|
-
await stateManager.putStorage(
|
|
122
|
-
createAddressFromString(address),
|
|
123
|
-
hexToBytes(addHexPrefix(String(keyHex ?? "").replace(/^0x/i, ""))),
|
|
124
|
-
hexToBytes(addHexPrefix(String(bigintToHex32(nextValue) ?? "").replace(/^0x/i, ""))),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export async function putStorageAndCapture(stateManager, address, keyHex, nextValue) {
|
|
129
|
-
await currentStorageBigInt(stateManager, address, keyHex);
|
|
130
|
-
await putStorageValue(stateManager, address, keyHex, nextValue);
|
|
131
|
-
return stateManager.captureStateSnapshot();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export async function initializePrivateStateSnapshot({ controllerAddress, vaultAddress, channelId }) {
|
|
135
|
-
const stateManager = new TokamakL2StateManager({ common: createTokamakL2Common() });
|
|
136
|
-
const addresses = [controllerAddress, vaultAddress].map((address) => createAddressFromString(address));
|
|
137
|
-
await stateManager._initializeForAddresses(addresses);
|
|
138
|
-
stateManager._channelId = channelId;
|
|
139
|
-
for (const address of addresses) {
|
|
140
|
-
stateManager._commitResolvedStorageEntries(address, []);
|
|
141
|
-
}
|
|
142
|
-
return stateManager.captureStateSnapshot();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function getBlockInfoAt(provider, blockNumber, { send = (method, params) => provider.send(method, params) } = {}) {
|
|
146
|
-
const blockTag = ethers.toQuantity(blockNumber);
|
|
147
|
-
const block = await send("eth_getBlockByNumber", [blockTag, false]);
|
|
148
|
-
const prevBlockHashes = [];
|
|
149
|
-
for (let offset = 1; offset <= tokamakPrevBlockHashCount; offset += 1) {
|
|
150
|
-
if (blockNumber <= offset) {
|
|
151
|
-
prevBlockHashes.push("0x0");
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const previousBlock = await send("eth_getBlockByNumber", [ethers.toQuantity(blockNumber - offset), false]);
|
|
155
|
-
prevBlockHashes.push(previousBlock.hash);
|
|
156
|
-
}
|
|
157
|
-
const chainId = await send("eth_chainId", []);
|
|
158
|
-
return {
|
|
159
|
-
coinBase: block.miner,
|
|
160
|
-
timeStamp: block.timestamp,
|
|
161
|
-
blockNumber: block.number,
|
|
162
|
-
prevRanDao: block.prevRandao ?? block.mixHash ?? block.difficulty ?? "0x0",
|
|
163
|
-
gasLimit: block.gasLimit,
|
|
164
|
-
chainId,
|
|
165
|
-
selfBalance: "0x0",
|
|
166
|
-
baseFee: block.baseFeePerGas ?? "0x0",
|
|
167
|
-
prevBlockHashes,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export async function getFixedBlockInfo(provider, options = {}) {
|
|
172
|
-
const send = options.send ?? ((method, params) => provider.send(method, params));
|
|
173
|
-
const latestNumberHex = await send("eth_blockNumber", []);
|
|
174
|
-
const latestNumber = Number(hexToBigInt(addHexPrefix(String(latestNumberHex ?? "").replace(/^0x/i, ""))));
|
|
175
|
-
return getBlockInfoAt(provider, latestNumber, { send });
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function deriveLiquidBalanceStorageKey(l2Address, slot) {
|
|
179
|
-
return normalizeBytes32Hex(bytesToHex(getUserStorageKey([l2Address, ethers.toBigInt(slot)], "TokamakL2")));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function deriveChannelTokenVaultLeafIndex(storageKey) {
|
|
183
|
-
return hexToBigInt(addHexPrefix(String(storageKey ?? "").replace(/^0x/i, ""))) % ethers.toBigInt(MAX_MT_LEAVES);
|
|
184
|
-
}
|