@agoric/cosmic-swingset 0.40.0 → 0.40.1-dev-800c2bb.0
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/Makefile +63 -34
- package/README-bridge.md +0 -3
- package/README.md +0 -2
- package/bin/ag-nchainz +1 -1
- package/bootstrap.json +2 -0
- package/calc-rpcport.js +2 -1
- package/check-validator.cjs +2 -0
- package/economy-template.json +67 -5
- package/jsconfig.json +2 -14
- package/package.json +20 -27
- package/rosetta.json +54 -0
- package/src/chain-main.js +422 -213
- package/src/entrypoint.js +3 -5
- package/src/export-kernel-db.js +430 -0
- package/src/export-storage.js +91 -0
- package/src/helpers/bufferedStorage.js +194 -0
- package/src/{is-entrypoint.js → helpers/is-entrypoint.js} +2 -0
- package/src/{json-stable-stringify.js → helpers/json-stable-stringify.js} +1 -1
- package/src/helpers/make-queue.js +134 -0
- package/src/helpers/process-value.js +133 -0
- package/src/import-kernel-db.js +207 -0
- package/src/kernel-stats.js +88 -18
- package/src/launch-chain.js +265 -206
- package/src/params.js +20 -21
- package/src/sim-chain.js +58 -54
- package/src/sim-params.js +4 -3
- package/transfer.ros +161 -0
- package/src/action-types.js +0 -13
- package/src/make-queue.js +0 -86
- package/src/validate-inbound.js +0 -109
package/src/entrypoint.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
+
// @jessie-check
|
|
2
3
|
|
|
3
|
-
import '@endo/init/pre
|
|
4
|
-
|
|
5
|
-
// import lmdb early to work around SES incompatibility
|
|
6
|
-
import 'lmdb';
|
|
4
|
+
import '@endo/init/pre.js';
|
|
7
5
|
|
|
8
6
|
import agcc from '@agoric/cosmos';
|
|
9
7
|
|
|
10
|
-
import '@endo/init';
|
|
8
|
+
import '@endo/init/unsafe-fast.js';
|
|
11
9
|
|
|
12
10
|
import os from 'os';
|
|
13
11
|
import path from 'path';
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
// @jessie-check
|
|
4
|
+
|
|
5
|
+
import '@endo/init/unsafe-fast.js';
|
|
6
|
+
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import process from 'process';
|
|
9
|
+
import fsPower from 'fs/promises';
|
|
10
|
+
import pathPower from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
import { makePromiseKit } from '@endo/promise-kit';
|
|
14
|
+
import { Fail } from '@agoric/assert';
|
|
15
|
+
import { makeAggregateError } from '@agoric/internal';
|
|
16
|
+
import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
|
|
17
|
+
import { openSwingStore, makeSwingStoreExporter } from '@agoric/swing-store';
|
|
18
|
+
|
|
19
|
+
import { isEntrypoint } from './helpers/is-entrypoint.js';
|
|
20
|
+
import { makeProcessValue } from './helpers/process-value.js';
|
|
21
|
+
|
|
22
|
+
/** @typedef {'current' | 'archival' | 'debug'} SwingStoreExportMode */
|
|
23
|
+
|
|
24
|
+
// eslint-disable-next-line jsdoc/require-returns-check
|
|
25
|
+
/**
|
|
26
|
+
* @param {string | undefined} mode
|
|
27
|
+
* @returns {asserts mode is SwingStoreExportMode | undefined}
|
|
28
|
+
*/
|
|
29
|
+
const checkExportMode = mode => {
|
|
30
|
+
switch (mode) {
|
|
31
|
+
case 'current':
|
|
32
|
+
case 'archival':
|
|
33
|
+
case 'debug':
|
|
34
|
+
case undefined:
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
throw Fail`Invalid value ${mode} for "export-mode"`;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A state-sync manifest is a representation of the information contained in a
|
|
43
|
+
* swingStore export for a given block.
|
|
44
|
+
*
|
|
45
|
+
* The `data` field is the name of a file containing the swingStore's KV
|
|
46
|
+
* "export data".
|
|
47
|
+
* The `artifacts` field is a list of [artifactName, fileName] pairs
|
|
48
|
+
* (where the content of each artifact is stored in the corresponding file).
|
|
49
|
+
* For more details, see packages/swing-store/docs/data-export.md
|
|
50
|
+
*
|
|
51
|
+
* @typedef {object} StateSyncManifest
|
|
52
|
+
* @property {number} blockHeight the block height corresponding to this export
|
|
53
|
+
* @property {SwingStoreExportMode} [mode]
|
|
54
|
+
* @property {string} [data] file name containing the swingStore "export data"
|
|
55
|
+
* @property {Array<[artifactName: string, fileName: string]>} artifacts
|
|
56
|
+
* List of swingStore export artifacts which can be validated by the export data
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {object} StateSyncExporter
|
|
61
|
+
* @property {() => number | undefined} getBlockHeight
|
|
62
|
+
* @property {() => Promise<void>} onStarted
|
|
63
|
+
* @property {() => Promise<void>} onDone
|
|
64
|
+
* @property {() => Promise<void>} stop
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @typedef {object} StateSyncExporterOptions
|
|
69
|
+
* @property {string} stateDir the directory containing the SwingStore to export
|
|
70
|
+
* @property {string} exportDir the directory in which to place the exported artifacts and manifest
|
|
71
|
+
* @property {number} [blockHeight] block height to check for
|
|
72
|
+
* @property {SwingStoreExportMode} [exportMode] whether to include historical or debug artifacts in the export
|
|
73
|
+
* @property {boolean} [includeExportData] whether to include an artifact for the export data in the export
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {StateSyncExporterOptions} options
|
|
78
|
+
* @param {object} powers
|
|
79
|
+
* @param {Pick<import('fs/promises'), 'open' | 'writeFile'>} powers.fs
|
|
80
|
+
* @param {import('path')['resolve']} powers.pathResolve
|
|
81
|
+
* @param {typeof import('@agoric/swing-store')['makeSwingStoreExporter']} [powers.makeSwingStoreExporter]
|
|
82
|
+
* @param {typeof import('@agoric/swing-store')['openSwingStore']} [powers.openSwingStore]
|
|
83
|
+
* @param {null | ((...args: any[]) => void)} [powers.log]
|
|
84
|
+
* @returns {StateSyncExporter}
|
|
85
|
+
*/
|
|
86
|
+
export const initiateSwingStoreExport = (
|
|
87
|
+
{ stateDir, exportDir, blockHeight, exportMode, includeExportData },
|
|
88
|
+
{
|
|
89
|
+
fs: { open, writeFile },
|
|
90
|
+
pathResolve,
|
|
91
|
+
makeSwingStoreExporter: makeExporter = makeSwingStoreExporter,
|
|
92
|
+
openSwingStore: openDB = openSwingStore,
|
|
93
|
+
log = console.log,
|
|
94
|
+
},
|
|
95
|
+
) => {
|
|
96
|
+
const effectiveExportMode = exportMode ?? 'current';
|
|
97
|
+
if (effectiveExportMode !== 'current' && !includeExportData) {
|
|
98
|
+
throw Fail`Must include export data if export mode not "current"`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @type {number | undefined} */
|
|
102
|
+
let savedBlockHeight;
|
|
103
|
+
|
|
104
|
+
/** @type {import('@endo/promise-kit').PromiseKit<void>} */
|
|
105
|
+
const startedKit = makePromiseKit();
|
|
106
|
+
|
|
107
|
+
/** @type {Error | undefined} */
|
|
108
|
+
let stopped;
|
|
109
|
+
const abortIfStopped = () => {
|
|
110
|
+
if (stopped) throw stopped;
|
|
111
|
+
};
|
|
112
|
+
/** @type {Array<() => Promise<void>>} */
|
|
113
|
+
const cleanup = [];
|
|
114
|
+
|
|
115
|
+
const exportDone = (async () => {
|
|
116
|
+
const manifestPath = pathResolve(exportDir, 'export-manifest.json');
|
|
117
|
+
const manifestFile = await open(manifestPath, 'wx');
|
|
118
|
+
cleanup.push(async () => manifestFile.close());
|
|
119
|
+
|
|
120
|
+
const swingStoreExporter = makeExporter(stateDir, exportMode);
|
|
121
|
+
cleanup.push(async () => swingStoreExporter.close());
|
|
122
|
+
|
|
123
|
+
const { hostStorage } = openDB(stateDir);
|
|
124
|
+
|
|
125
|
+
savedBlockHeight = Number(hostStorage.kvStore.get('host.height')) || 0;
|
|
126
|
+
await hostStorage.close();
|
|
127
|
+
|
|
128
|
+
if (blockHeight) {
|
|
129
|
+
blockHeight === savedBlockHeight ||
|
|
130
|
+
Fail`DB at unexpected block height ${savedBlockHeight} (expected ${blockHeight})`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
abortIfStopped();
|
|
134
|
+
startedKit.resolve();
|
|
135
|
+
log?.(`Starting DB export at block height ${savedBlockHeight}`);
|
|
136
|
+
|
|
137
|
+
/** @type {StateSyncManifest} */
|
|
138
|
+
const manifest = {
|
|
139
|
+
blockHeight: savedBlockHeight,
|
|
140
|
+
mode: exportMode,
|
|
141
|
+
artifacts: [],
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (includeExportData) {
|
|
145
|
+
log?.(`Writing Export Data`);
|
|
146
|
+
const fileName = `export-data.jsonl`;
|
|
147
|
+
// eslint-disable-next-line @jessie.js/no-nested-await
|
|
148
|
+
const exportDataFile = await open(pathResolve(exportDir, fileName), 'wx');
|
|
149
|
+
cleanup.push(async () => exportDataFile.close());
|
|
150
|
+
|
|
151
|
+
// eslint-disable-next-line @jessie.js/no-nested-await
|
|
152
|
+
for await (const entry of swingStoreExporter.getExportData()) {
|
|
153
|
+
await exportDataFile.write(`${JSON.stringify(entry)}\n`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
manifest.data = fileName;
|
|
157
|
+
}
|
|
158
|
+
abortIfStopped();
|
|
159
|
+
|
|
160
|
+
for await (const artifactName of swingStoreExporter.getArtifactNames()) {
|
|
161
|
+
abortIfStopped();
|
|
162
|
+
log?.(`Writing artifact: ${artifactName}`);
|
|
163
|
+
const artifactData = swingStoreExporter.getArtifact(artifactName);
|
|
164
|
+
// Use artifactName as the file name as we trust swingStore to generate
|
|
165
|
+
// artifact names that are valid file names.
|
|
166
|
+
await writeFile(pathResolve(exportDir, artifactName), artifactData);
|
|
167
|
+
manifest.artifacts.push([artifactName, artifactName]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await manifestFile.write(JSON.stringify(manifest, null, 2));
|
|
171
|
+
log?.(`Saved export manifest: ${manifestPath}`);
|
|
172
|
+
})();
|
|
173
|
+
|
|
174
|
+
const doCleanup = async opErr => {
|
|
175
|
+
const errors = [];
|
|
176
|
+
for await (const cleaner of cleanup.reverse()) {
|
|
177
|
+
await Promise.resolve()
|
|
178
|
+
.then(cleaner)
|
|
179
|
+
.catch(err => errors.push(err));
|
|
180
|
+
}
|
|
181
|
+
if (errors.length) {
|
|
182
|
+
const error = makeAggregateError(errors, 'Errors while cleaning up');
|
|
183
|
+
if (opErr) {
|
|
184
|
+
Object.defineProperty(error, 'cause', { value: opErr });
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
} else if (opErr) {
|
|
188
|
+
throw opErr;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const done = exportDone.then(doCleanup, doCleanup).catch(err => {
|
|
193
|
+
startedKit.reject(err);
|
|
194
|
+
throw err;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
getBlockHeight: () => blockHeight || savedBlockHeight,
|
|
199
|
+
onStarted: async () => startedKit.promise,
|
|
200
|
+
onDone: async () => done,
|
|
201
|
+
stop: async () => {
|
|
202
|
+
stopped ||= Error(`Interrupted`);
|
|
203
|
+
return done.catch(err => {
|
|
204
|
+
if (err !== stopped) {
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @typedef {{type: 'started', blockHeight: number}} ExportMessageStarted
|
|
214
|
+
* @typedef {{type: 'done', error?: Error}} ExportMessageDone
|
|
215
|
+
* @typedef {ExportMessageStarted | ExportMessageDone} ExportMessage
|
|
216
|
+
*/
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {string[]} args
|
|
220
|
+
* @param {object} powers
|
|
221
|
+
* @param {Partial<Record<string, string>>} powers.env
|
|
222
|
+
* @param {string} powers.homedir
|
|
223
|
+
* @param {((msg: ExportMessage) => void) | null} powers.send
|
|
224
|
+
* @param {Console} powers.console
|
|
225
|
+
* @param {import('fs/promises')} powers.fs
|
|
226
|
+
* @param {import('path')['resolve']} powers.pathResolve
|
|
227
|
+
*/
|
|
228
|
+
export const main = async (
|
|
229
|
+
args,
|
|
230
|
+
{ env, homedir, send, console, fs, pathResolve },
|
|
231
|
+
) => {
|
|
232
|
+
const processValue = makeProcessValue({ env, args });
|
|
233
|
+
|
|
234
|
+
const stateDir =
|
|
235
|
+
processValue.getFlag('state-dir') ||
|
|
236
|
+
// We try to find the actual cosmos state directory (default=~/.ag-chain-cosmos)
|
|
237
|
+
`${processValue.getFlag(
|
|
238
|
+
'home',
|
|
239
|
+
`${homedir}/.ag-chain-cosmos`,
|
|
240
|
+
)}/data/agoric`;
|
|
241
|
+
|
|
242
|
+
const stateDirStat = await fs.stat(stateDir);
|
|
243
|
+
if (!stateDirStat.isDirectory()) {
|
|
244
|
+
throw Error('state-dir must be an exiting directory');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const exportDir = pathResolve(
|
|
248
|
+
/** @type {string} */ (processValue.getFlag('export-dir', '.')),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const includeExportData = processValue.getBoolean({
|
|
252
|
+
flagName: 'include-export-data',
|
|
253
|
+
});
|
|
254
|
+
const exportMode = processValue.getFlag('export-mode');
|
|
255
|
+
checkExportMode(exportMode);
|
|
256
|
+
|
|
257
|
+
const checkBlockHeight = processValue.getInteger({
|
|
258
|
+
flagName: 'check-block-height',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const verbose = processValue.getBoolean({
|
|
262
|
+
flagName: 'verbose',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const { registerShutdown } = makeShutdown(false);
|
|
266
|
+
|
|
267
|
+
const exporter = initiateSwingStoreExport(
|
|
268
|
+
{
|
|
269
|
+
stateDir,
|
|
270
|
+
exportDir,
|
|
271
|
+
blockHeight: checkBlockHeight,
|
|
272
|
+
exportMode,
|
|
273
|
+
includeExportData,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
fs,
|
|
277
|
+
pathResolve,
|
|
278
|
+
log: verbose ? console.log : null,
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
registerShutdown(() => exporter.stop());
|
|
283
|
+
|
|
284
|
+
exporter.onStarted().then(
|
|
285
|
+
() =>
|
|
286
|
+
send?.({
|
|
287
|
+
type: 'started',
|
|
288
|
+
blockHeight: /** @type {number} */ (exporter.getBlockHeight()),
|
|
289
|
+
}),
|
|
290
|
+
() => {},
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
await exporter.onDone().then(
|
|
294
|
+
() => send?.({ type: 'done' }),
|
|
295
|
+
error => {
|
|
296
|
+
verbose && console.error('state-sync export failed', error);
|
|
297
|
+
if (send) {
|
|
298
|
+
send({ type: 'done', error });
|
|
299
|
+
} else {
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {StateSyncExporterOptions} options
|
|
308
|
+
* @param {object} powers
|
|
309
|
+
* @param {typeof import('child_process')['fork']} powers.fork
|
|
310
|
+
* @param {boolean} [powers.verbose]
|
|
311
|
+
* @returns {StateSyncExporter}
|
|
312
|
+
*/
|
|
313
|
+
export const spawnSwingStoreExport = (
|
|
314
|
+
{ stateDir, exportDir, blockHeight, exportMode, includeExportData },
|
|
315
|
+
{ fork, verbose },
|
|
316
|
+
) => {
|
|
317
|
+
const args = ['--state-dir', stateDir, '--export-dir', exportDir];
|
|
318
|
+
|
|
319
|
+
if (blockHeight !== undefined) {
|
|
320
|
+
args.push('--check-block-height', String(blockHeight));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (exportMode) {
|
|
324
|
+
args.push('--export-mode', exportMode);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (includeExportData) {
|
|
328
|
+
args.push('--include-export-data');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (verbose) {
|
|
332
|
+
args.push('--verbose');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const cp = fork(fileURLToPath(import.meta.url), args, {
|
|
336
|
+
serialization: 'advanced', // To get error objects serialized
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const kits = harden({
|
|
340
|
+
/** @type {import('@endo/promise-kit').PromiseKit<void>} */
|
|
341
|
+
started: makePromiseKit(),
|
|
342
|
+
/** @type {import('@endo/promise-kit').PromiseKit<void>} */
|
|
343
|
+
done: makePromiseKit(),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
let exited = false;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {number | null} code
|
|
350
|
+
* @param {NodeJS.Signals | null} signal
|
|
351
|
+
*/
|
|
352
|
+
const onExit = (code, signal) => {
|
|
353
|
+
exited = true;
|
|
354
|
+
kits.done.reject(
|
|
355
|
+
Error(`Process exited before done. code=${code}, signal=${signal}`),
|
|
356
|
+
);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** @type {number | undefined} */
|
|
360
|
+
let exportBlockHeight;
|
|
361
|
+
|
|
362
|
+
/** @param {import('./export-kernel-db.js').ExportMessage} msg */
|
|
363
|
+
const onMessage = msg => {
|
|
364
|
+
switch (msg.type) {
|
|
365
|
+
case 'started': {
|
|
366
|
+
exportBlockHeight = msg.blockHeight;
|
|
367
|
+
kits.started.resolve();
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'done': {
|
|
371
|
+
if (msg.error) {
|
|
372
|
+
kits.done.reject(msg.error);
|
|
373
|
+
} else {
|
|
374
|
+
kits.done.resolve();
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
default: {
|
|
379
|
+
// @ts-expect-error exhaustive check
|
|
380
|
+
Fail`Unexpected ${msg.type} message`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
cp.on('error', kits.done.reject).on('exit', onExit).on('message', onMessage);
|
|
386
|
+
|
|
387
|
+
void kits.done.promise
|
|
388
|
+
.catch(err => {
|
|
389
|
+
kits.started.reject(err);
|
|
390
|
+
})
|
|
391
|
+
.then(() => {
|
|
392
|
+
cp.off('error', kits.done.reject)
|
|
393
|
+
.off('exit', onExit)
|
|
394
|
+
.off('message', onMessage);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
if (cp.exitCode != null) {
|
|
398
|
+
onExit(cp.exitCode, null);
|
|
399
|
+
}
|
|
400
|
+
return harden({
|
|
401
|
+
getBlockHeight: () => blockHeight || exportBlockHeight,
|
|
402
|
+
onStarted: async () => kits.started.promise,
|
|
403
|
+
onDone: async () => kits.done.promise,
|
|
404
|
+
stop: async () => {
|
|
405
|
+
if (!exited) {
|
|
406
|
+
cp.kill();
|
|
407
|
+
}
|
|
408
|
+
return kits.done.promise;
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
if (isEntrypoint(import.meta.url)) {
|
|
414
|
+
main(process.argv.splice(2), {
|
|
415
|
+
homedir: os.homedir(),
|
|
416
|
+
env: process.env,
|
|
417
|
+
send: process.send
|
|
418
|
+
? Function.prototype.bind.call(process.send, process)
|
|
419
|
+
: undefined,
|
|
420
|
+
console,
|
|
421
|
+
fs: fsPower,
|
|
422
|
+
pathResolve: pathPower.resolve,
|
|
423
|
+
}).then(
|
|
424
|
+
_res => 0,
|
|
425
|
+
rej => {
|
|
426
|
+
console.error(`error running export-kernel-db:`, rej);
|
|
427
|
+
process.exit(process.exitCode || rej.exitCode || 1);
|
|
428
|
+
},
|
|
429
|
+
);
|
|
430
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js';
|
|
2
|
+
|
|
3
|
+
const { Fail, quote: q } = assert;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Export any specified storage subtrees, then delete the ones marked to clear.
|
|
7
|
+
*
|
|
8
|
+
* @param {(method: string, args: unknown[]) => any} batchChainStorage send a
|
|
9
|
+
* batch command over the chain storage bridge
|
|
10
|
+
* @param {string[]} [exportStorageSubtrees] chain storage paths identifying
|
|
11
|
+
* roots of subtrees for which data should be exported into bootstrap vat
|
|
12
|
+
* parameter `chainStorageEntries` (e.g., `exportStorageSubtrees: ['c.o']`
|
|
13
|
+
* might result in vatParameters including `chainStorageEntries: [ ['c.o',
|
|
14
|
+
* '"top"'], ['c.o.i'], ['c.o.i.n', '42'], ['c.o.w', '"moo"'] ]`).
|
|
15
|
+
* @param {string[]} [clearStorageSubtrees] chain storage paths identifying
|
|
16
|
+
* roots of subtrees for which data should be deleted (TODO: including
|
|
17
|
+
* overlaps with exportStorageSubtrees, which are *not* preserved).
|
|
18
|
+
*/
|
|
19
|
+
export const exportStorage = (
|
|
20
|
+
batchChainStorage,
|
|
21
|
+
exportStorageSubtrees = [],
|
|
22
|
+
clearStorageSubtrees = [],
|
|
23
|
+
) => {
|
|
24
|
+
const chainStorageEntries = [];
|
|
25
|
+
|
|
26
|
+
const isInSubtree = (path, root) =>
|
|
27
|
+
path === root || path.startsWith(`${root}.`);
|
|
28
|
+
|
|
29
|
+
const singleChainStorage = (method, path) =>
|
|
30
|
+
batchChainStorage(method, [path]);
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
// Disallow exporting internal details like bundle contents and the action queue.
|
|
34
|
+
const exportRoot = STORAGE_PATH.CUSTOM;
|
|
35
|
+
const badPaths = exportStorageSubtrees.filter(
|
|
36
|
+
path => !isInSubtree(path, exportRoot),
|
|
37
|
+
);
|
|
38
|
+
badPaths.length === 0 ||
|
|
39
|
+
// prettier-ignore
|
|
40
|
+
Fail`Exported chain storage paths ${q(badPaths)} must start with ${q(exportRoot)}`;
|
|
41
|
+
|
|
42
|
+
const makeExportEntry = (path, value) =>
|
|
43
|
+
value == null ? [path] : [path, value];
|
|
44
|
+
|
|
45
|
+
// Preserve the ordering of each subtree via depth-first traversal.
|
|
46
|
+
let pendingEntries = exportStorageSubtrees.map(path => {
|
|
47
|
+
const value = singleChainStorage('get', path);
|
|
48
|
+
return makeExportEntry(path, value);
|
|
49
|
+
});
|
|
50
|
+
while (pendingEntries.length > 0) {
|
|
51
|
+
const entry = /** @type {[string, string?]} */ (pendingEntries.shift());
|
|
52
|
+
chainStorageEntries.push(entry);
|
|
53
|
+
const [path, _value] = entry;
|
|
54
|
+
const childEntryData = singleChainStorage('entries', path);
|
|
55
|
+
const childEntries = childEntryData.map(([pathSegment, value]) => {
|
|
56
|
+
return makeExportEntry(`${path}.${pathSegment}`, value);
|
|
57
|
+
});
|
|
58
|
+
pendingEntries = [...childEntries, ...pendingEntries];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
// Clear other chain storage data as configured.
|
|
64
|
+
// NOTE THAT WE DO NOT LIMIT THIS TO THE CUSTOM PATH!
|
|
65
|
+
// USE AT YOUR OWN RISK!
|
|
66
|
+
const pathsToClear = [];
|
|
67
|
+
const batchThreshold = 100;
|
|
68
|
+
const sendBatch = () => {
|
|
69
|
+
// Consume pathsToClear and map each path to a no-value entry.
|
|
70
|
+
const args = pathsToClear.splice(0).map(path => [path]);
|
|
71
|
+
batchChainStorage('setWithoutNotify', args);
|
|
72
|
+
};
|
|
73
|
+
let pathsToCheck = [...clearStorageSubtrees];
|
|
74
|
+
while (pathsToCheck.length > 0) {
|
|
75
|
+
const path = pathsToCheck.shift();
|
|
76
|
+
pathsToClear.push(path);
|
|
77
|
+
if (pathsToClear.length >= batchThreshold) {
|
|
78
|
+
sendBatch();
|
|
79
|
+
}
|
|
80
|
+
const childPaths = singleChainStorage('children', path).map(
|
|
81
|
+
segment => `${path}.${segment}`,
|
|
82
|
+
);
|
|
83
|
+
pathsToCheck = [...childPaths, ...pathsToCheck];
|
|
84
|
+
}
|
|
85
|
+
if (pathsToClear.length > 0) {
|
|
86
|
+
sendBatch();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return chainStorageEntries;
|
|
91
|
+
};
|