@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/src/entrypoint.js CHANGED
@@ -1,13 +1,11 @@
1
1
  #! /usr/bin/env node
2
+ // @jessie-check
2
3
 
3
- import '@endo/init/pre-bundle-source.js';
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
+ };