@tokamak-private-dapps/private-state-cli 2.2.0 → 2.2.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 +6 -0
- package/README.md +6 -0
- package/lib/private-state-cli-command-registry.mjs +11 -3
- package/lib/private-state-runtime-management.mjs +21 -26
- package/lib/runtime.mjs +267 -72
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 2.2.1 - 2026-05-18
|
|
6
|
+
|
|
7
|
+
- Added `channel recover-workspace --source rpc --output-raw` to append raw JSON-RPC request and response history
|
|
8
|
+
to method-specific JSON files under the channel workspace `rpcCallHistory/` directory, with `eth_getLogs` split by event.
|
|
9
|
+
Indexed recovery appends to existing history; `--from-genesis` overwrites it with one full genesis-to-latest scan.
|
|
10
|
+
|
|
5
11
|
## 2.2.0 - 2026-05-18
|
|
6
12
|
|
|
7
13
|
- Added `install --read-only` for channel-state read commands and commands that do not depend on channel state. This
|
package/README.md
CHANGED
|
@@ -192,6 +192,12 @@ run is interrupted, the next non-`--from-genesis` RPC recovery resumes from the
|
|
|
192
192
|
can also start from that local checkpoint: it uses a matching delta bundle when one is available, otherwise a newer
|
|
193
193
|
verified full mirror checkpoint replaces the local checkpoint before RPC catch-up.
|
|
194
194
|
|
|
195
|
+
Use `channel recover-workspace --source rpc --output-raw` when you need to preserve the raw JSON-RPC request and
|
|
196
|
+
response history for inspection. The CLI appends calls to method-specific JSON files, and splits `eth_getLogs` into
|
|
197
|
+
event-specific files such as `eth_getLogs.CurrentRootVectorObserved.json`, under
|
|
198
|
+
`~/tokamak-private-channels/workspace/<network>/<channel>/channel/rpcCallHistory/`. Indexed recovery appends to the
|
|
199
|
+
existing history, while `--from-genesis` overwrites it with one full genesis-to-latest scan.
|
|
200
|
+
|
|
195
201
|
`channel create` is the exception: after the channel is created on-chain, the CLI initializes that new local workspace
|
|
196
202
|
by replaying from the channel's genesis block because no prior recovery index can exist for a new channel.
|
|
197
203
|
|
|
@@ -202,6 +202,13 @@ export const PRIVATE_STATE_CLI_FIELD_CATALOG = Object.freeze({
|
|
|
202
202
|
option: "--from-genesis",
|
|
203
203
|
optional: true,
|
|
204
204
|
},
|
|
205
|
+
outputRaw: {
|
|
206
|
+
label: "Output Raw RPC History",
|
|
207
|
+
type: "checkbox",
|
|
208
|
+
hint: "With channel recover-workspace --source rpc, preserve raw JSON-RPC request and response history under the channel workspace.",
|
|
209
|
+
option: "--output-raw",
|
|
210
|
+
optional: true,
|
|
211
|
+
},
|
|
205
212
|
source: {
|
|
206
213
|
label: "Recovery Source",
|
|
207
214
|
type: "select",
|
|
@@ -383,8 +390,8 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
383
390
|
display: "channel recover-workspace",
|
|
384
391
|
description: "Rebuild the local channel workspace from bridge state.",
|
|
385
392
|
installMode: "read-only",
|
|
386
|
-
fields: ["channelName", "network", "source", "fromGenesis"],
|
|
387
|
-
usage: "--channel-name, --network, optional --source, optional --from-genesis",
|
|
393
|
+
fields: ["channelName", "network", "source", "fromGenesis", "outputRaw"],
|
|
394
|
+
usage: "--channel-name, --network, optional --source, optional --from-genesis, optional --output-raw",
|
|
388
395
|
help: [
|
|
389
396
|
"By default, --source rpc resumes RPC log scanning from the workspace recovery index when available",
|
|
390
397
|
"--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",
|
|
@@ -392,6 +399,7 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
|
|
|
392
399
|
"Mirror recovery uses a matching delta bundle when available; otherwise a newer verified full checkpoint replaces the local checkpoint before RPC catch-up",
|
|
393
400
|
"Fails instead of falling back to genesis when no usable recovery index exists",
|
|
394
401
|
"Use --source rpc --from-genesis to ignore the recovery index and replay logs from channel genesis",
|
|
402
|
+
"--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",
|
|
395
403
|
"--from-genesis moves the existing local channel workspace to workspace-rebuild-backups before writing the current-format workspace; local secrets are preserved",
|
|
396
404
|
"Prints RPC log scan progress while rebuilding the workspace",
|
|
397
405
|
],
|
|
@@ -731,7 +739,7 @@ export function privateStateCliCommandSynopsis(command) {
|
|
|
731
739
|
return null;
|
|
732
740
|
}
|
|
733
741
|
const valueLabel = field.valueLabel ?? field.placeholderLabel ?? `<${field.label?.toUpperCase().replace(/\s+/g, "_") ?? "VALUE"}>`;
|
|
734
|
-
const option = field.type === "checkbox"
|
|
742
|
+
const option = field.type === "checkbox"
|
|
735
743
|
? field.option
|
|
736
744
|
: `${field.option} ${valueLabel}`;
|
|
737
745
|
return field.optional || optionalFields.has(fieldKey) ? `[${option}]` : option;
|
|
@@ -108,10 +108,6 @@ function runCaptured(command, args, { cwd = defaultCommandCwd, env = process.env
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
function requireSemverVersion(value, label) {
|
|
112
|
-
return requireExactSemverVersion(value, label);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
111
|
function readTokamakCliPackageReport(packageRoot = null) {
|
|
116
112
|
try {
|
|
117
113
|
const resolvedPackageRoot = packageRoot ?? resolveActiveTokamakCliPackageRoot();
|
|
@@ -234,19 +230,17 @@ function buildDoctorHumanRows(report) {
|
|
|
234
230
|
check: `${report.installManifest.mode} deployment artifacts`,
|
|
235
231
|
status: doctorStatus(report.checks.find((check) => check.name === `${report.installManifest.mode} deployment artifacts`)?.ok),
|
|
236
232
|
detail: [
|
|
237
|
-
`readOnlyChains=${report.deploymentArtifacts.
|
|
238
|
-
|
|
239
|
-
.map((entry) => entry.chainId)
|
|
240
|
-
.join(",") || "none"}`,
|
|
241
|
-
`fullChains=${report.deploymentArtifacts.chains
|
|
242
|
-
.filter((entry) => entry.modes[PRIVATE_STATE_INSTALL_MODES.FULL].ok)
|
|
243
|
-
.map((entry) => entry.chainId)
|
|
244
|
-
.join(",") || "none"}`,
|
|
233
|
+
`readOnlyChains=${formatInstalledArtifactChains(report.deploymentArtifacts, PRIVATE_STATE_INSTALL_MODES.READ_ONLY)}`,
|
|
234
|
+
`fullChains=${formatInstalledArtifactChains(report.deploymentArtifacts, PRIVATE_STATE_INSTALL_MODES.FULL)}`,
|
|
245
235
|
].join(" "),
|
|
246
236
|
},
|
|
247
237
|
];
|
|
248
238
|
}
|
|
249
239
|
|
|
240
|
+
function formatInstalledArtifactChains(artifactReadiness, mode) {
|
|
241
|
+
return installedArtifactChainIds(artifactReadiness, mode).join(",") || "none";
|
|
242
|
+
}
|
|
243
|
+
|
|
250
244
|
function formatDoctorTable(rows) {
|
|
251
245
|
const headers = ["Check", "Status", "Detail"];
|
|
252
246
|
const checkWidth = Math.max(headers[0].length, ...rows.map((row) => row.check.length));
|
|
@@ -660,19 +654,20 @@ function buildCommandAvailability({ artifactReadiness, installMode, tokamakCli,
|
|
|
660
654
|
requiredInstallMode,
|
|
661
655
|
available,
|
|
662
656
|
chains: requiredInstallMode === PRIVATE_STATE_INSTALL_MODES.FULL
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
: requiredInstallMode === PRIVATE_STATE_INSTALL_MODES.READ_ONLY
|
|
667
|
-
? artifactReadiness.chains
|
|
668
|
-
.filter((entry) => entry.modes[PRIVATE_STATE_INSTALL_MODES.READ_ONLY].ok)
|
|
669
|
-
.map((entry) => entry.chainId)
|
|
670
|
-
: [],
|
|
657
|
+
|| requiredInstallMode === PRIVATE_STATE_INSTALL_MODES.READ_ONLY
|
|
658
|
+
? installedArtifactChainIds(artifactReadiness, requiredInstallMode)
|
|
659
|
+
: [],
|
|
671
660
|
reasons,
|
|
672
661
|
};
|
|
673
662
|
});
|
|
674
663
|
}
|
|
675
664
|
|
|
665
|
+
function installedArtifactChainIds(artifactReadiness, mode) {
|
|
666
|
+
return artifactReadiness.chains
|
|
667
|
+
.filter((entry) => entry.modes[mode]?.ok)
|
|
668
|
+
.map((entry) => entry.chainId);
|
|
669
|
+
}
|
|
670
|
+
|
|
676
671
|
function buildSelectedRuntimeVersionCheck({ installManifest, installMode, tokamakCli, groth16Runtime }) {
|
|
677
672
|
if (installMode === PRIVATE_STATE_INSTALL_MODES.READ_ONLY) {
|
|
678
673
|
return {
|
|
@@ -752,16 +747,16 @@ async function resolveRequestedGroth16PackageVersion(requestedVersion) {
|
|
|
752
747
|
}
|
|
753
748
|
|
|
754
749
|
const bundledPackageJson = readJson(path.join(resolveGroth16PackageRoot(), "package.json"));
|
|
755
|
-
return
|
|
750
|
+
return requireExactSemverVersion(bundledPackageJson.version, `${GROTH16_PACKAGE_NAME} bundled package version`);
|
|
756
751
|
}
|
|
757
752
|
|
|
758
753
|
async function resolveRequestedNpmPackageVersion({ packageName, requestedVersion, optionName }) {
|
|
759
754
|
const metadata = await fetchNpmPackageMetadata(packageName);
|
|
760
755
|
if (requestedVersion === undefined || requestedVersion === null) {
|
|
761
|
-
return
|
|
756
|
+
return requireExactSemverVersion(metadata?.["dist-tags"]?.latest, `${packageName} npm latest version`);
|
|
762
757
|
}
|
|
763
758
|
|
|
764
|
-
const normalizedVersion =
|
|
759
|
+
const normalizedVersion = requireExactSemverVersion(requestedVersion, optionName);
|
|
765
760
|
if (!metadata.versions?.[normalizedVersion]) {
|
|
766
761
|
throw new Error(`npm package ${packageName} does not contain version ${normalizedVersion}.`);
|
|
767
762
|
}
|
|
@@ -832,7 +827,7 @@ async function installGroth16RuntimeForPrivateState({ version, docker }) {
|
|
|
832
827
|
}
|
|
833
828
|
|
|
834
829
|
function resolveGroth16RuntimePackageInstall(version) {
|
|
835
|
-
const normalizedVersion =
|
|
830
|
+
const normalizedVersion = requireExactSemverVersion(version, `${GROTH16_PACKAGE_NAME} version`);
|
|
836
831
|
const bundledPackageRoot = resolveGroth16PackageRoot();
|
|
837
832
|
const bundledPackageJson = readJson(path.join(bundledPackageRoot, "package.json"));
|
|
838
833
|
if (bundledPackageJson.name === GROTH16_PACKAGE_NAME && bundledPackageJson.version === normalizedVersion) {
|
|
@@ -930,7 +925,7 @@ function parseDriveFileIdFromDownloadUrl(value) {
|
|
|
930
925
|
|
|
931
926
|
function installManagedNpmPackage({ packageName, version, cacheBaseRoot = resolveArtifactCacheBaseRoot() }) {
|
|
932
927
|
const normalizedPackageName = requireNonEmptyString(packageName, "packageName");
|
|
933
|
-
const normalizedVersion =
|
|
928
|
+
const normalizedVersion = requireExactSemverVersion(version, `${normalizedPackageName} version`);
|
|
934
929
|
const installPrefix = managedNpmPackageInstallPrefix({
|
|
935
930
|
packageName: normalizedPackageName,
|
|
936
931
|
version: normalizedVersion,
|
|
@@ -965,7 +960,7 @@ function managedNpmPackageInstallPrefix({ packageName, version, cacheBaseRoot =
|
|
|
965
960
|
const safePackageName = requireNonEmptyString(packageName, "packageName")
|
|
966
961
|
.replace(/^@/, "")
|
|
967
962
|
.replace(/[^A-Za-z0-9._-]+/g, "__");
|
|
968
|
-
return path.join(privateStateCliRuntimeRoot(cacheBaseRoot), "npm", safePackageName,
|
|
963
|
+
return path.join(privateStateCliRuntimeRoot(cacheBaseRoot), "npm", safePackageName, requireExactSemverVersion(version, "version"));
|
|
969
964
|
}
|
|
970
965
|
|
|
971
966
|
async function downloadGroth16CrsArtifactsForPrivateState({
|
package/lib/runtime.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
deriveChannelTokenVaultLeafIndex,
|
|
53
53
|
deriveLiquidBalanceStorageKey,
|
|
54
54
|
fetchContractCodes,
|
|
55
|
+
getBlockInfoAt,
|
|
55
56
|
normalizeBytesHex,
|
|
56
57
|
normalizeBytes32Hex,
|
|
57
58
|
serializeBigInts,
|
|
@@ -554,25 +555,25 @@ function requireFlatDeploymentArtifactPathsForChainId(chainId) {
|
|
|
554
555
|
}
|
|
555
556
|
|
|
556
557
|
function requireInstalledDeploymentArtifacts(artifactPaths, chainId, mode) {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (!fs.existsSync(entry.path)) {
|
|
561
|
-
throw new Error(`Missing ${entry.label}: ${entry.path}.`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
} catch (error) {
|
|
565
|
-
throw cliError(
|
|
566
|
-
CLI_ERROR_CODES.MISSING_DEPLOYMENT_ARTIFACTS,
|
|
567
|
-
[
|
|
568
|
-
`Missing ${mode} installed deployment artifacts for chain ${chainId} under ${artifactPaths.rootDir}.`,
|
|
569
|
-
mode === PRIVATE_STATE_INSTALL_MODES.FULL
|
|
570
|
-
? "Run install before running private-state CLI commands that write channel state."
|
|
571
|
-
: "Run install --read-only before running private-state CLI commands that read channel state.",
|
|
572
|
-
`Original error: ${error.message}`,
|
|
573
|
-
].join(" "),
|
|
574
|
-
);
|
|
558
|
+
const missingFiles = missingInstalledDeploymentArtifactFiles(artifactPaths, mode);
|
|
559
|
+
if (missingFiles.length === 0) {
|
|
560
|
+
return;
|
|
575
561
|
}
|
|
562
|
+
throw cliError(
|
|
563
|
+
CLI_ERROR_CODES.MISSING_DEPLOYMENT_ARTIFACTS,
|
|
564
|
+
[
|
|
565
|
+
`Missing ${mode} installed deployment artifacts for chain ${chainId} under ${artifactPaths.rootDir}.`,
|
|
566
|
+
mode === PRIVATE_STATE_INSTALL_MODES.FULL
|
|
567
|
+
? "Run install before running private-state CLI commands that write channel state."
|
|
568
|
+
: "Run install --read-only before running private-state CLI commands that read channel state.",
|
|
569
|
+
`Original error: ${missingFiles.map((entry) => `Missing ${entry.label}: ${entry.path}.`).join(" ")}`,
|
|
570
|
+
].join(" "),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function missingInstalledDeploymentArtifactFiles(artifactPaths, mode) {
|
|
575
|
+
return privateStateCliArtifactRequiredFiles(artifactPaths, mode)
|
|
576
|
+
.filter((entry) => !fs.existsSync(entry.path));
|
|
576
577
|
}
|
|
577
578
|
|
|
578
579
|
async function handleChannelCreate({ args, network, provider }) {
|
|
@@ -721,12 +722,14 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
721
722
|
const workspaceName = channelName;
|
|
722
723
|
const bridgeResources = loadBridgeResources({ chainId: network.chainId });
|
|
723
724
|
const recoverySource = resolveWorkspaceRecoverySource(args);
|
|
725
|
+
const outputRawRpcCallHistory = args.outputRaw === true;
|
|
724
726
|
|
|
725
727
|
const {
|
|
726
728
|
workspaceDir,
|
|
727
729
|
workspace,
|
|
728
730
|
currentSnapshot,
|
|
729
731
|
cleanRebuildBackup,
|
|
732
|
+
rpcCallHistory,
|
|
730
733
|
} = await syncChannelWorkspace({
|
|
731
734
|
workspaceName,
|
|
732
735
|
channelName,
|
|
@@ -738,6 +741,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
738
741
|
useWorkspaceRecoveryIndex: true,
|
|
739
742
|
fromGenesis: args.fromGenesis === true,
|
|
740
743
|
recoverySource,
|
|
744
|
+
outputRawRpcCallHistory,
|
|
741
745
|
progressAction: "channel recover-workspace",
|
|
742
746
|
});
|
|
743
747
|
|
|
@@ -757,6 +761,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
|
|
|
757
761
|
recoveryLastScannedBlock: workspace.recoveryLastScannedBlock,
|
|
758
762
|
recoveryRootVectorHash: workspace.recoveryRootVectorHash,
|
|
759
763
|
recoveryScanRange: workspace.recoveryScanRange,
|
|
764
|
+
rpcCallHistory,
|
|
760
765
|
workspaceMirror: workspace.workspaceMirror ?? null,
|
|
761
766
|
});
|
|
762
767
|
}
|
|
@@ -1968,6 +1973,7 @@ async function syncChannelWorkspace({
|
|
|
1968
1973
|
useWorkspaceRecoveryIndex = false,
|
|
1969
1974
|
fromGenesis = false,
|
|
1970
1975
|
recoverySource = "rpc",
|
|
1976
|
+
outputRawRpcCallHistory = false,
|
|
1971
1977
|
minimumToBlock = null,
|
|
1972
1978
|
progressAction = null,
|
|
1973
1979
|
}) {
|
|
@@ -1993,8 +1999,15 @@ async function syncChannelWorkspace({
|
|
|
1993
1999
|
});
|
|
1994
2000
|
}
|
|
1995
2001
|
|
|
2002
|
+
const rpcCallHistoryRecorder = outputRawRpcCallHistory
|
|
2003
|
+
? createRpcCallHistoryRecorder({ workspaceDir })
|
|
2004
|
+
: null;
|
|
2005
|
+
const activeProvider = rpcCallHistoryRecorder
|
|
2006
|
+
? attachRpcCallHistoryRecorderToProvider(provider, rpcCallHistoryRecorder)
|
|
2007
|
+
: provider;
|
|
2008
|
+
|
|
1996
2009
|
const { bridgeDeployment, bridgeAbiManifest } = bridgeResources;
|
|
1997
|
-
const bridgeCore = new Contract(bridgeDeployment.bridgeCore, bridgeAbiManifest.contracts.bridgeCore.abi,
|
|
2010
|
+
const bridgeCore = new Contract(bridgeDeployment.bridgeCore, bridgeAbiManifest.contracts.bridgeCore.abi, activeProvider);
|
|
1998
2011
|
const channelId = deriveChannelIdFromName(channelName);
|
|
1999
2012
|
const channelInfo = await bridgeCore.getChannel(channelId);
|
|
2000
2013
|
if (!channelInfo.exists) {
|
|
@@ -2004,13 +2017,13 @@ async function syncChannelWorkspace({
|
|
|
2004
2017
|
const channelManager = new Contract(
|
|
2005
2018
|
channelInfo.manager,
|
|
2006
2019
|
bridgeAbiManifest.contracts.channelManager.abi,
|
|
2007
|
-
|
|
2020
|
+
activeProvider,
|
|
2008
2021
|
);
|
|
2009
2022
|
const canonicalAsset = getAddress(channelInfo.asset);
|
|
2010
|
-
const canonicalAssetDecimals = await fetchTokenDecimals(
|
|
2023
|
+
const canonicalAssetDecimals = await fetchTokenDecimals(activeProvider, canonicalAsset);
|
|
2011
2024
|
const currentRootVectorHash = normalizeBytes32Hex(await channelManager.currentRootVectorHash());
|
|
2012
2025
|
const genesisBlockNumber = Number(await channelManager.genesisBlockNumber());
|
|
2013
|
-
const observedLatestBlock = await
|
|
2026
|
+
const observedLatestBlock = await activeProvider.getBlockNumber();
|
|
2014
2027
|
const latestBlock = minimumToBlock === null
|
|
2015
2028
|
? observedLatestBlock
|
|
2016
2029
|
: Math.max(observedLatestBlock, Number(minimumToBlock));
|
|
@@ -2038,8 +2051,8 @@ async function syncChannelWorkspace({
|
|
|
2038
2051
|
`Managed storage vector does not include L2 accounting vault ${l2AccountingVaultAddress}.`,
|
|
2039
2052
|
);
|
|
2040
2053
|
|
|
2041
|
-
const contractCodes = await fetchContractCodes(
|
|
2042
|
-
const blockInfo = await
|
|
2054
|
+
const contractCodes = await fetchContractCodes(activeProvider, managedStorageAddresses);
|
|
2055
|
+
const blockInfo = await getBlockInfoAt(activeProvider, genesisBlockNumber);
|
|
2043
2056
|
const derivedAPubBlockHash = normalizeBytes32Hex(hashTokamakPublicInputs(encodeTokamakBlockInfo(blockInfo)));
|
|
2044
2057
|
expect(
|
|
2045
2058
|
ethers.toBigInt(derivedAPubBlockHash) === ethers.toBigInt(normalizeBytes32Hex(channelInfo.aPubBlockHash)),
|
|
@@ -2154,6 +2167,10 @@ async function syncChannelWorkspace({
|
|
|
2154
2167
|
});
|
|
2155
2168
|
}
|
|
2156
2169
|
: null;
|
|
2170
|
+
rpcCallHistoryRecorder?.setScanRange({
|
|
2171
|
+
fromBlock: selectedRecoveryIndex?.nextBlock ?? genesisBlockNumber,
|
|
2172
|
+
toBlock: latestBlock,
|
|
2173
|
+
});
|
|
2157
2174
|
const reconstruction = localSnapshotReusable
|
|
2158
2175
|
? {
|
|
2159
2176
|
currentSnapshot: existingArtifacts.stateSnapshot,
|
|
@@ -2173,7 +2190,7 @@ async function syncChannelWorkspace({
|
|
|
2173
2190
|
},
|
|
2174
2191
|
}
|
|
2175
2192
|
: await reconstructChannelSnapshot({
|
|
2176
|
-
provider,
|
|
2193
|
+
provider: activeProvider,
|
|
2177
2194
|
bridgeAbiManifest,
|
|
2178
2195
|
channelInfo,
|
|
2179
2196
|
channelManager,
|
|
@@ -2190,7 +2207,9 @@ async function syncChannelWorkspace({
|
|
|
2190
2207
|
toBlock: latestBlock,
|
|
2191
2208
|
progressAction,
|
|
2192
2209
|
onCheckpoint: persistWorkspaceCheckpoint,
|
|
2210
|
+
rpcCallHistoryRecorder,
|
|
2193
2211
|
});
|
|
2212
|
+
rpcCallHistoryRecorder?.setScanRange(reconstruction.scanRange);
|
|
2194
2213
|
const currentSnapshot = reconstruction.currentSnapshot;
|
|
2195
2214
|
const workspace = buildWorkspaceForSnapshot({
|
|
2196
2215
|
currentSnapshot,
|
|
@@ -2215,6 +2234,7 @@ async function syncChannelWorkspace({
|
|
|
2215
2234
|
blockInfo,
|
|
2216
2235
|
contractCodes,
|
|
2217
2236
|
cleanRebuildBackup,
|
|
2237
|
+
rpcCallHistory: rpcCallHistoryRecorder?.finish() ?? null,
|
|
2218
2238
|
};
|
|
2219
2239
|
}
|
|
2220
2240
|
|
|
@@ -2656,6 +2676,198 @@ function persistChannelWorkspaceFiles({
|
|
|
2656
2676
|
writeJsonIfChanged(channelWorkspaceConfigPath(workspaceDir), workspace);
|
|
2657
2677
|
}
|
|
2658
2678
|
|
|
2679
|
+
function channelWorkspaceRpcCallHistoryPath(workspaceDir) {
|
|
2680
|
+
return path.join(channelDataPath(workspaceDir), "rpcCallHistory");
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function createRpcCallHistoryRecorder({ workspaceDir }) {
|
|
2684
|
+
const historyDir = channelWorkspaceRpcCallHistoryPath(workspaceDir);
|
|
2685
|
+
const entriesByFile = new Map();
|
|
2686
|
+
let scanRange = null;
|
|
2687
|
+
let callCount = 0;
|
|
2688
|
+
ensureDir(historyDir);
|
|
2689
|
+
|
|
2690
|
+
const pushEntry = ({ method, eventName = null, entry }) => {
|
|
2691
|
+
const file = rpcCallHistoryFileName(method, eventName);
|
|
2692
|
+
const entries = entriesByFile.get(file) ?? [];
|
|
2693
|
+
entries.push({
|
|
2694
|
+
method,
|
|
2695
|
+
...(eventName ? { event: eventName } : {}),
|
|
2696
|
+
...entry,
|
|
2697
|
+
});
|
|
2698
|
+
entriesByFile.set(file, entries);
|
|
2699
|
+
};
|
|
2700
|
+
|
|
2701
|
+
return {
|
|
2702
|
+
historyDir,
|
|
2703
|
+
setScanRange(nextScanRange) {
|
|
2704
|
+
scanRange = {
|
|
2705
|
+
fromBlock: Number(nextScanRange.fromBlock),
|
|
2706
|
+
toBlock: Number(nextScanRange.toBlock),
|
|
2707
|
+
...(nextScanRange.mode ? { mode: nextScanRange.mode } : {}),
|
|
2708
|
+
};
|
|
2709
|
+
},
|
|
2710
|
+
recordRpcCall({ method, params, response, error = null }) {
|
|
2711
|
+
if (method === "eth_getLogs") {
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
callCount += 1;
|
|
2715
|
+
pushEntry({
|
|
2716
|
+
method,
|
|
2717
|
+
entry: {
|
|
2718
|
+
recordedAt: new Date().toISOString(),
|
|
2719
|
+
request: buildRawJsonRpcRequest(method, params),
|
|
2720
|
+
...(error ? { error } : { response }),
|
|
2721
|
+
},
|
|
2722
|
+
});
|
|
2723
|
+
},
|
|
2724
|
+
recordEthGetLogs({ request, logs, groupedValues, chunkFromBlock, chunkToBlock }) {
|
|
2725
|
+
callCount += 1;
|
|
2726
|
+
const eventBuckets = groupRawEthGetLogsByRecoveryEvent({ logs, groupedValues });
|
|
2727
|
+
for (const [eventName, response] of eventBuckets.entries()) {
|
|
2728
|
+
pushEntry({
|
|
2729
|
+
method: "eth_getLogs",
|
|
2730
|
+
eventName,
|
|
2731
|
+
entry: {
|
|
2732
|
+
recordedAt: new Date().toISOString(),
|
|
2733
|
+
chunkRange: { fromBlock: Number(chunkFromBlock), toBlock: Number(chunkToBlock) },
|
|
2734
|
+
request: buildRawEthGetLogsRequest(request),
|
|
2735
|
+
response,
|
|
2736
|
+
},
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
},
|
|
2740
|
+
finish() {
|
|
2741
|
+
const files = [...entriesByFile.entries()].map(([file, entries]) =>
|
|
2742
|
+
appendRpcCallHistoryEntries({
|
|
2743
|
+
historyDir,
|
|
2744
|
+
file,
|
|
2745
|
+
entries: entries.map((entry) => ({ scanRange, ...entry })),
|
|
2746
|
+
}));
|
|
2747
|
+
return {
|
|
2748
|
+
historyDir,
|
|
2749
|
+
scanRange,
|
|
2750
|
+
callCount,
|
|
2751
|
+
files: files.sort((left, right) => left.file.localeCompare(right.file)),
|
|
2752
|
+
};
|
|
2753
|
+
},
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
function attachRpcCallHistoryRecorderToProvider(provider, recorder) {
|
|
2758
|
+
const send = provider.send.bind(provider);
|
|
2759
|
+
provider.send = async (method, params) => {
|
|
2760
|
+
try {
|
|
2761
|
+
const response = await send(method, params);
|
|
2762
|
+
recorder.recordRpcCall({ method, params, response });
|
|
2763
|
+
return response;
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
recorder.recordRpcCall({ method, params, error: normalizeRpcCallHistoryError(error) });
|
|
2766
|
+
throw error;
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
return provider;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
function normalizeRpcCallHistoryError(error) {
|
|
2773
|
+
return {
|
|
2774
|
+
name: error?.name ?? "Error",
|
|
2775
|
+
code: error?.code ?? null,
|
|
2776
|
+
message: error?.message ?? String(error),
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function appendRpcCallHistoryEntries({ historyDir, file, entries }) {
|
|
2781
|
+
const filePath = path.join(historyDir, file);
|
|
2782
|
+
const { method, event: eventName = null } = entries[0];
|
|
2783
|
+
const current = readJsonIfExists(filePath) ?? {
|
|
2784
|
+
method,
|
|
2785
|
+
...(eventName ? { event: eventName } : {}),
|
|
2786
|
+
entries: [],
|
|
2787
|
+
};
|
|
2788
|
+
expect(current.method === method, `RPC call history file method mismatch: ${filePath}.`);
|
|
2789
|
+
expect(Array.isArray(current.entries), `RPC call history file entries must be an array: ${filePath}.`);
|
|
2790
|
+
if (eventName) {
|
|
2791
|
+
expect(current.event === eventName, `RPC call history file event mismatch: ${filePath}.`);
|
|
2792
|
+
}
|
|
2793
|
+
current.updatedAt = new Date().toISOString();
|
|
2794
|
+
current.entries.push(...entries);
|
|
2795
|
+
writeJson(filePath, current);
|
|
2796
|
+
return {
|
|
2797
|
+
method,
|
|
2798
|
+
...(eventName ? { event: eventName } : {}),
|
|
2799
|
+
file,
|
|
2800
|
+
path: filePath,
|
|
2801
|
+
entriesAdded: entries.length,
|
|
2802
|
+
totalEntries: current.entries.length,
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
function buildRawJsonRpcRequest(method, params = []) {
|
|
2807
|
+
return {
|
|
2808
|
+
jsonrpc: "2.0",
|
|
2809
|
+
method,
|
|
2810
|
+
params: params ?? [],
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
function rpcCallHistoryFileName(method, eventName = null) {
|
|
2815
|
+
const suffix = eventName ? `.${safeRpcCallHistoryFileToken(eventName)}` : "";
|
|
2816
|
+
return `${safeRpcCallHistoryFileToken(method)}${suffix}.json`;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function safeRpcCallHistoryFileToken(value) {
|
|
2820
|
+
return String(value).replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function groupRawEthGetLogsByRecoveryEvent({ logs, groupedValues }) {
|
|
2824
|
+
if (logs.length === 0) {
|
|
2825
|
+
return new Map([["noLogs", []]]);
|
|
2826
|
+
}
|
|
2827
|
+
const eventNamesByLog = new Map();
|
|
2828
|
+
for (const group of groupedValues) {
|
|
2829
|
+
for (const event of group) {
|
|
2830
|
+
eventNamesByLog.set(recoveryLogHistoryKey(event), channelRecoveryEventName(event));
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
const buckets = new Map();
|
|
2834
|
+
for (const log of logs) {
|
|
2835
|
+
const eventName = eventNamesByLog.get(recoveryLogHistoryKey(log)) ?? "unknown";
|
|
2836
|
+
const bucket = buckets.get(eventName) ?? [];
|
|
2837
|
+
bucket.push(log);
|
|
2838
|
+
buckets.set(eventName, bucket);
|
|
2839
|
+
}
|
|
2840
|
+
return buckets;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
function recoveryLogHistoryKey(log) {
|
|
2844
|
+
return `${normalizeBytes32Hex(log.transactionHash)}:${Number(log.index ?? log.logIndex)}`;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
function channelRecoveryEventName(event) {
|
|
2848
|
+
if (event.fragment?.name) {
|
|
2849
|
+
return event.fragment.name;
|
|
2850
|
+
}
|
|
2851
|
+
const topic0 = event.topics[0] ? normalizeBytes32Hex(event.topics[0]) : null;
|
|
2852
|
+
if (topic0 === normalizeBytes32Hex(CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC)) {
|
|
2853
|
+
return "StorageKeyObserved";
|
|
2854
|
+
}
|
|
2855
|
+
if (topic0 === normalizeBytes32Hex(VAULT_STORAGE_WRITE_OBSERVED_TOPIC)) {
|
|
2856
|
+
return "LiquidBalanceStorageWriteObserved";
|
|
2857
|
+
}
|
|
2858
|
+
return "unknown";
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
function buildRawEthGetLogsRequest(request) {
|
|
2862
|
+
const filter = {
|
|
2863
|
+
address: request.address,
|
|
2864
|
+
topics: request.topics,
|
|
2865
|
+
fromBlock: ethers.toQuantity(request.fromBlock),
|
|
2866
|
+
toBlock: ethers.toQuantity(request.toBlock),
|
|
2867
|
+
};
|
|
2868
|
+
return buildRawJsonRpcRequest("eth_getLogs", [filter]);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2659
2871
|
function nextAvailablePath(basePath) {
|
|
2660
2872
|
if (!fs.existsSync(basePath)) {
|
|
2661
2873
|
return basePath;
|
|
@@ -3623,12 +3835,10 @@ function inspectGuideNetworkRuntime(networkName) {
|
|
|
3623
3835
|
|
|
3624
3836
|
function inspectGuideDeploymentArtifacts(chainId) {
|
|
3625
3837
|
const paths = privateStateCliArtifactPaths(resolveArtifactCacheBaseRoot(), chainId);
|
|
3626
|
-
const readOnlyMissingFiles =
|
|
3627
|
-
.map((entry) => entry.path)
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
.map((entry) => entry.path)
|
|
3631
|
-
.filter((filePath) => !fs.existsSync(filePath));
|
|
3838
|
+
const readOnlyMissingFiles = missingInstalledDeploymentArtifactFiles(paths, PRIVATE_STATE_INSTALL_MODES.READ_ONLY)
|
|
3839
|
+
.map((entry) => entry.path);
|
|
3840
|
+
const fullMissingFiles = missingInstalledDeploymentArtifactFiles(paths, PRIVATE_STATE_INSTALL_MODES.FULL)
|
|
3841
|
+
.map((entry) => entry.path);
|
|
3632
3842
|
return {
|
|
3633
3843
|
installed: readOnlyMissingFiles.length === 0,
|
|
3634
3844
|
readOnlyInstalled: readOnlyMissingFiles.length === 0,
|
|
@@ -8437,39 +8647,6 @@ function appendSplitWord(target, startIndex, value) {
|
|
|
8437
8647
|
target[startIndex + 1] = normalized >> 128n;
|
|
8438
8648
|
}
|
|
8439
8649
|
|
|
8440
|
-
async function fetchChannelBlockInfo(provider, blockNumber) {
|
|
8441
|
-
const blockTag = ethers.toQuantity(blockNumber);
|
|
8442
|
-
const block = await provider.send("eth_getBlockByNumber", [blockTag, false]);
|
|
8443
|
-
if (!block) {
|
|
8444
|
-
throw new Error(`Unable to fetch channel genesis block ${blockNumber}.`);
|
|
8445
|
-
}
|
|
8446
|
-
|
|
8447
|
-
const prevBlockHashes = [];
|
|
8448
|
-
for (let offset = 1; offset <= TOKAMAK_PREVIOUS_BLOCK_HASH_COUNT; offset += 1) {
|
|
8449
|
-
if (blockNumber <= offset) {
|
|
8450
|
-
prevBlockHashes.push("0x0");
|
|
8451
|
-
continue;
|
|
8452
|
-
}
|
|
8453
|
-
const previousBlock = await provider.send("eth_getBlockByNumber", [ethers.toQuantity(blockNumber - offset), false]);
|
|
8454
|
-
if (!previousBlock) {
|
|
8455
|
-
throw new Error(`Unable to fetch previous block hash for block ${blockNumber - offset}.`);
|
|
8456
|
-
}
|
|
8457
|
-
prevBlockHashes.push(previousBlock.hash);
|
|
8458
|
-
}
|
|
8459
|
-
|
|
8460
|
-
return {
|
|
8461
|
-
coinBase: block.miner,
|
|
8462
|
-
timeStamp: block.timestamp,
|
|
8463
|
-
blockNumber: block.number,
|
|
8464
|
-
prevRanDao: block.prevRandao ?? block.mixHash ?? block.difficulty ?? "0x0",
|
|
8465
|
-
gasLimit: block.gasLimit,
|
|
8466
|
-
chainId: await provider.send("eth_chainId", []),
|
|
8467
|
-
selfBalance: "0x0",
|
|
8468
|
-
baseFee: block.baseFeePerGas ?? "0x0",
|
|
8469
|
-
prevBlockHashes,
|
|
8470
|
-
};
|
|
8471
|
-
}
|
|
8472
|
-
|
|
8473
8650
|
async function fetchChannelRecoveryLogs({
|
|
8474
8651
|
provider,
|
|
8475
8652
|
bridgeAbiManifest,
|
|
@@ -8510,6 +8687,7 @@ async function fetchChannelRecoveryEventGroupsChunked({
|
|
|
8510
8687
|
fromBlock,
|
|
8511
8688
|
toBlock,
|
|
8512
8689
|
progressAction = null,
|
|
8690
|
+
rpcCallHistoryRecorder = null,
|
|
8513
8691
|
onChunk,
|
|
8514
8692
|
}) {
|
|
8515
8693
|
const recoveryFilter = buildChannelRecoveryLogFilter({
|
|
@@ -8527,7 +8705,7 @@ async function fetchChannelRecoveryEventGroupsChunked({
|
|
|
8527
8705
|
onProgress: progressAction
|
|
8528
8706
|
? createRpcLogScanProgress({ action: progressAction, label: "channel-recovery chunks" })
|
|
8529
8707
|
: null,
|
|
8530
|
-
onChunk: async ({ logs, chunkFromBlock, chunkToBlock }) => {
|
|
8708
|
+
onChunk: async ({ request, logs, chunkFromBlock, chunkToBlock }) => {
|
|
8531
8709
|
const groupedValues = normalizeWorkspaceMirrorDeltaEventGroups({
|
|
8532
8710
|
logs,
|
|
8533
8711
|
channelInfo,
|
|
@@ -8535,6 +8713,13 @@ async function fetchChannelRecoveryEventGroupsChunked({
|
|
|
8535
8713
|
fromBlock: chunkFromBlock,
|
|
8536
8714
|
toBlock: chunkToBlock,
|
|
8537
8715
|
});
|
|
8716
|
+
rpcCallHistoryRecorder?.recordEthGetLogs({
|
|
8717
|
+
request,
|
|
8718
|
+
logs,
|
|
8719
|
+
groupedValues,
|
|
8720
|
+
chunkFromBlock,
|
|
8721
|
+
chunkToBlock,
|
|
8722
|
+
});
|
|
8538
8723
|
await onChunk?.({
|
|
8539
8724
|
groupedValues,
|
|
8540
8725
|
chunkFromBlock,
|
|
@@ -8591,6 +8776,7 @@ async function reconstructChannelSnapshot({
|
|
|
8591
8776
|
toBlock = null,
|
|
8592
8777
|
progressAction = null,
|
|
8593
8778
|
onCheckpoint = null,
|
|
8779
|
+
rpcCallHistoryRecorder = null,
|
|
8594
8780
|
}) {
|
|
8595
8781
|
let startingSnapshot = baseSnapshot;
|
|
8596
8782
|
if (!startingSnapshot) {
|
|
@@ -8627,6 +8813,7 @@ async function reconstructChannelSnapshot({
|
|
|
8627
8813
|
fromBlock: scanFromBlock,
|
|
8628
8814
|
toBlock: latestBlock,
|
|
8629
8815
|
progressAction,
|
|
8816
|
+
rpcCallHistoryRecorder,
|
|
8630
8817
|
onChunk: async ({ groupedValues, chunkFromBlock, chunkToBlock }) => {
|
|
8631
8818
|
currentSnapshot = await applyChannelRecoveryEventGroupsToStateManager({
|
|
8632
8819
|
stateManager,
|
|
@@ -8979,13 +9166,14 @@ async function fetchLogsChunked(provider, {
|
|
|
8979
9166
|
while (cursor <= resolvedToBlock) {
|
|
8980
9167
|
const chunkToBlock = Math.min(resolvedToBlock, cursor + chunkSize - 1);
|
|
8981
9168
|
let logs;
|
|
9169
|
+
const request = {
|
|
9170
|
+
address,
|
|
9171
|
+
topics,
|
|
9172
|
+
fromBlock: cursor,
|
|
9173
|
+
toBlock: chunkToBlock,
|
|
9174
|
+
};
|
|
8982
9175
|
try {
|
|
8983
|
-
logs = await fetchLogsRateLimited(provider,
|
|
8984
|
-
address,
|
|
8985
|
-
topics,
|
|
8986
|
-
fromBlock: cursor,
|
|
8987
|
-
toBlock: chunkToBlock,
|
|
8988
|
-
});
|
|
9176
|
+
logs = await fetchLogsRateLimited(provider, request);
|
|
8989
9177
|
} catch (error) {
|
|
8990
9178
|
throw buildRpcLogQueryConfigError({
|
|
8991
9179
|
error,
|
|
@@ -9019,6 +9207,7 @@ async function fetchLogsChunked(provider, {
|
|
|
9019
9207
|
totalBlocks,
|
|
9020
9208
|
logsFound,
|
|
9021
9209
|
chunkLogs: logs.length,
|
|
9210
|
+
request,
|
|
9022
9211
|
logs,
|
|
9023
9212
|
});
|
|
9024
9213
|
cursor = chunkToBlock + 1;
|
|
@@ -10154,10 +10343,16 @@ function assertCreateChannelArgs(args) {
|
|
|
10154
10343
|
|
|
10155
10344
|
function assertRecoverWorkspaceArgs(args) {
|
|
10156
10345
|
assertAllowedCommandSchema(args, "channel-recover-workspace");
|
|
10157
|
-
resolveWorkspaceRecoverySource(args);
|
|
10346
|
+
const source = resolveWorkspaceRecoverySource(args);
|
|
10158
10347
|
if (args.fromGenesis !== undefined && args.fromGenesis !== true) {
|
|
10159
10348
|
throw new Error("channel recover-workspace option --from-genesis does not accept a value.");
|
|
10160
10349
|
}
|
|
10350
|
+
if (args.outputRaw !== undefined && args.outputRaw !== true) {
|
|
10351
|
+
throw new Error("channel recover-workspace option --output-raw does not accept a value.");
|
|
10352
|
+
}
|
|
10353
|
+
if (args.outputRaw === true && source !== "rpc") {
|
|
10354
|
+
throw new Error("channel recover-workspace option --output-raw requires --source rpc.");
|
|
10355
|
+
}
|
|
10161
10356
|
}
|
|
10162
10357
|
|
|
10163
10358
|
function assertGetChannelArgs(args) {
|
package/package.json
CHANGED