@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/chain-main.js
CHANGED
|
@@ -1,30 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { resolve as pathResolve } from 'path';
|
|
2
4
|
import v8 from 'node:v8';
|
|
3
5
|
import process from 'node:process';
|
|
4
6
|
import fs from 'node:fs';
|
|
7
|
+
import fsPromises from 'node:fs/promises';
|
|
5
8
|
import { performance } from 'perf_hooks';
|
|
6
9
|
import { resolve as importMetaResolve } from 'import-meta-resolve';
|
|
10
|
+
import tmpfs from 'tmp';
|
|
11
|
+
import { fork } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
import { E } from '@endo/far';
|
|
7
14
|
import engineGC from '@agoric/swingset-vat/src/lib-nodejs/engine-gc.js';
|
|
8
15
|
import { waitUntilQuiescent } from '@agoric/swingset-vat/src/lib-nodejs/waitUntilQuiescent.js';
|
|
9
16
|
import {
|
|
10
17
|
importMailbox,
|
|
11
18
|
exportMailbox,
|
|
12
19
|
} from '@agoric/swingset-vat/src/devices/mailbox/mailbox.js';
|
|
13
|
-
import { makeBufferedStorage } from '@agoric/swingset-vat/src/lib/storageAPI.js';
|
|
14
20
|
|
|
15
|
-
import {
|
|
16
|
-
import { makeSlogSender } from '@agoric/telemetry';
|
|
21
|
+
import { Fail, q } from '@agoric/assert';
|
|
22
|
+
import { makeSlogSender, tryFlushSlogSender } from '@agoric/telemetry';
|
|
17
23
|
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
makeChainStorageRoot,
|
|
26
|
+
makeSerializeToStorage,
|
|
27
|
+
} from '@agoric/internal/src/lib-chainStorage.js';
|
|
19
28
|
import { makeMarshal } from '@endo/marshal';
|
|
20
|
-
import {
|
|
29
|
+
import { makeShutdown } from '@agoric/internal/src/node/shutdown.js';
|
|
21
30
|
|
|
22
|
-
import * as STORAGE_PATH from '@agoric/
|
|
31
|
+
import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js';
|
|
32
|
+
import * as ActionType from '@agoric/internal/src/action-types.js';
|
|
23
33
|
import { BridgeId as BRIDGE_ID } from '@agoric/internal';
|
|
24
|
-
import
|
|
34
|
+
import {
|
|
35
|
+
makeBufferedStorage,
|
|
36
|
+
makeReadCachingStorage,
|
|
37
|
+
} from './helpers/bufferedStorage.js';
|
|
38
|
+
import stringify from './helpers/json-stable-stringify.js';
|
|
25
39
|
import { launch } from './launch-chain.js';
|
|
26
40
|
import { getTelemetryProviders } from './kernel-stats.js';
|
|
27
|
-
import {
|
|
41
|
+
import { makeProcessValue } from './helpers/process-value.js';
|
|
42
|
+
import { spawnSwingStoreExport } from './export-kernel-db.js';
|
|
43
|
+
import { performStateSyncImport } from './import-kernel-db.js';
|
|
28
44
|
|
|
29
45
|
// eslint-disable-next-line no-unused-vars
|
|
30
46
|
let whenHellFreezesOver = null;
|
|
@@ -36,83 +52,70 @@ const TELEMETRY_SERVICE_NAME = 'agd-cosmos';
|
|
|
36
52
|
const toNumber = specimen => {
|
|
37
53
|
const number = parseInt(specimen, 10);
|
|
38
54
|
String(number) === String(specimen) ||
|
|
39
|
-
|
|
55
|
+
Fail`Could not parse ${JSON.stringify(specimen)} as a number`;
|
|
40
56
|
return number;
|
|
41
57
|
};
|
|
42
58
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* @template {unknown} [T=unknown]
|
|
61
|
+
* @param {(req: string) => string} call
|
|
62
|
+
* @param {string} prefix
|
|
63
|
+
* @param {"set" | "legacySet" | "setWithoutNotify"} setterMethod
|
|
64
|
+
* @param {(value: string) => T} fromBridgeStringValue
|
|
65
|
+
* @param {(value: T) => string} toBridgeStringValue
|
|
66
|
+
* @returns {import("./helpers/bufferedStorage.js").KVStore<T>}
|
|
67
|
+
*/
|
|
68
|
+
const makePrefixedBridgeStorage = (
|
|
69
|
+
call,
|
|
70
|
+
prefix,
|
|
71
|
+
setterMethod,
|
|
72
|
+
fromBridgeStringValue = x => JSON.parse(x),
|
|
73
|
+
toBridgeStringValue = x => stringify(x),
|
|
74
|
+
) => {
|
|
75
|
+
prefix.endsWith('.') || Fail`prefix ${prefix} must end with a dot`;
|
|
76
|
+
|
|
77
|
+
return harden({
|
|
78
|
+
has: key => {
|
|
79
|
+
const retStr = call(
|
|
80
|
+
stringify({ method: 'has', args: [`${prefix}${key}`] }),
|
|
81
|
+
);
|
|
82
|
+
const ret = JSON.parse(retStr);
|
|
83
|
+
return ret;
|
|
60
84
|
},
|
|
61
|
-
get
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const retStr = call(stringify({ method: 'get', key: `${prefix}${key}` }));
|
|
85
|
+
get: key => {
|
|
86
|
+
const retStr = call(
|
|
87
|
+
stringify({ method: 'get', args: [`${prefix}${key}`] }),
|
|
88
|
+
);
|
|
66
89
|
const ret = JSON.parse(retStr);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
: fromChainShape(chainShapeValue);
|
|
72
|
-
cache.set(key, value);
|
|
73
|
-
return value;
|
|
90
|
+
if (ret == null) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return fromBridgeStringValue(ret);
|
|
74
94
|
},
|
|
75
|
-
set(key, value) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// *during* a commit).
|
|
79
|
-
cache.set(key, value);
|
|
80
|
-
const chainShapeValue =
|
|
81
|
-
value === undefined || !toChainShape ? value : toChainShape(value);
|
|
82
|
-
const valueStr = value === undefined ? '' : stringify(chainShapeValue);
|
|
95
|
+
set: (key, value) => {
|
|
96
|
+
const path = `${prefix}${key}`;
|
|
97
|
+
const entry = [path, toBridgeStringValue(value)];
|
|
83
98
|
call(
|
|
84
99
|
stringify({
|
|
85
|
-
method:
|
|
86
|
-
|
|
87
|
-
value: valueStr,
|
|
100
|
+
method: setterMethod,
|
|
101
|
+
args: [entry],
|
|
88
102
|
}),
|
|
89
103
|
);
|
|
90
104
|
},
|
|
91
|
-
delete
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
delete: key => {
|
|
106
|
+
const path = `${prefix}${key}`;
|
|
107
|
+
const entry = [path];
|
|
108
|
+
call(
|
|
109
|
+
stringify({
|
|
110
|
+
method: setterMethod,
|
|
111
|
+
args: [entry],
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
98
114
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
kvStore: buffered,
|
|
102
|
-
commit,
|
|
103
|
-
abort,
|
|
104
|
-
} = makeBufferedStorage(storage, {
|
|
105
|
-
// Enqueue a write of any retrieved value, to handle callers like mailbox.js
|
|
106
|
-
// that expect local mutations to be automatically written back.
|
|
107
|
-
onGet(key, value) {
|
|
108
|
-
buffered.set(key, value);
|
|
115
|
+
getNextKey(_previousKey) {
|
|
116
|
+
throw Error('not implemented');
|
|
109
117
|
},
|
|
110
|
-
|
|
111
|
-
// Reset the read cache upon commit or abort.
|
|
112
|
-
onCommit: resetCache,
|
|
113
|
-
onAbort: resetCache,
|
|
114
118
|
});
|
|
115
|
-
return { ...buffered, commit, abort };
|
|
116
119
|
};
|
|
117
120
|
|
|
118
121
|
export default async function main(progname, args, { env, homedir, agcc }) {
|
|
@@ -120,32 +123,16 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
120
123
|
|
|
121
124
|
// TODO: use the 'basedir' pattern
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
function getFlagValue(flagName, deflt) {
|
|
125
|
-
let flagValue = deflt;
|
|
126
|
-
const envValue =
|
|
127
|
-
env[`AG_CHAIN_COSMOS_${flagName.toUpperCase().replace(/-/g, '_')}`];
|
|
128
|
-
if (envValue !== undefined) {
|
|
129
|
-
flagValue = envValue;
|
|
130
|
-
}
|
|
131
|
-
const flag = `--${flagName}`;
|
|
132
|
-
const flagEquals = `--${flagName}=`;
|
|
133
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
134
|
-
const arg = args[i];
|
|
135
|
-
if (arg === flag) {
|
|
136
|
-
i += 1;
|
|
137
|
-
flagValue = args[i];
|
|
138
|
-
} else if (arg.startsWith(flagEquals)) {
|
|
139
|
-
flagValue = arg.substr(flagEquals.length);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return flagValue;
|
|
143
|
-
}
|
|
126
|
+
const processValue = makeProcessValue({ env, args });
|
|
144
127
|
|
|
145
128
|
// We try to find the actual cosmos state directory (default=~/.ag-chain-cosmos), which
|
|
146
129
|
// is better than scribbling into the current directory.
|
|
147
|
-
const cosmosHome =
|
|
148
|
-
|
|
130
|
+
const cosmosHome = processValue.getFlag(
|
|
131
|
+
'home',
|
|
132
|
+
`${homedir}/.ag-chain-cosmos`,
|
|
133
|
+
);
|
|
134
|
+
const stateDBDir = `${cosmosHome}/data/agoric`;
|
|
135
|
+
fs.mkdirSync(stateDBDir, { recursive: true });
|
|
149
136
|
|
|
150
137
|
// console.log('Have AG_COSMOS', agcc);
|
|
151
138
|
|
|
@@ -154,14 +141,11 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
154
141
|
function registerPortHandler(portHandler) {
|
|
155
142
|
lastPort += 1;
|
|
156
143
|
const port = lastPort;
|
|
157
|
-
portHandlers[port] = async (...phArgs) =>
|
|
158
|
-
|
|
159
|
-
return await portHandler(...phArgs);
|
|
160
|
-
} catch (e) {
|
|
144
|
+
portHandlers[port] = async (...phArgs) =>
|
|
145
|
+
E.resolve(portHandler(...phArgs)).catch(e => {
|
|
161
146
|
console.error('portHandler threw', e);
|
|
162
147
|
throw e;
|
|
163
|
-
}
|
|
164
|
-
};
|
|
148
|
+
});
|
|
165
149
|
return port;
|
|
166
150
|
}
|
|
167
151
|
|
|
@@ -174,7 +158,8 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
174
158
|
}
|
|
175
159
|
const action = JSON.parse(str);
|
|
176
160
|
const p = Promise.resolve(handler(action));
|
|
177
|
-
|
|
161
|
+
void E.when(
|
|
162
|
+
p,
|
|
178
163
|
res => {
|
|
179
164
|
// console.error(`Replying in Node to ${str} with`, res);
|
|
180
165
|
replier.resolve(stringify(res !== undefined ? res : null));
|
|
@@ -195,6 +180,23 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
195
180
|
whenHellFreezesOver = new Promise(() => {});
|
|
196
181
|
agcc.runAgCosmosDaemon(nodePort, fromGo, [progname, ...args]);
|
|
197
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @type {undefined | {
|
|
185
|
+
* blockHeight: number,
|
|
186
|
+
* exporter?: import('./export-kernel-db.js').StateSyncExporter,
|
|
187
|
+
* exportDir?: string,
|
|
188
|
+
* cleanup?: () => Promise<void>,
|
|
189
|
+
* }}
|
|
190
|
+
*/
|
|
191
|
+
let stateSyncExport;
|
|
192
|
+
|
|
193
|
+
async function discardStateSyncExport() {
|
|
194
|
+
const exportData = stateSyncExport;
|
|
195
|
+
stateSyncExport = undefined;
|
|
196
|
+
await exportData?.exporter?.stop();
|
|
197
|
+
await exportData?.cleanup?.();
|
|
198
|
+
}
|
|
199
|
+
|
|
198
200
|
let savedChainSends = [];
|
|
199
201
|
|
|
200
202
|
// Send a chain downcall, recording what we sent and received.
|
|
@@ -204,7 +206,10 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
204
206
|
return ret;
|
|
205
207
|
}
|
|
206
208
|
|
|
207
|
-
const clearChainSends = () => {
|
|
209
|
+
const clearChainSends = async () => {
|
|
210
|
+
// Cosmos should have blocked before calling commit, but wait just in case
|
|
211
|
+
await stateSyncExport?.exporter?.onStarted().catch(() => {});
|
|
212
|
+
|
|
208
213
|
const chainSends = savedChainSends;
|
|
209
214
|
savedChainSends = [];
|
|
210
215
|
return chainSends;
|
|
@@ -232,37 +237,81 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
232
237
|
}
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
/** @type {((obj: object) => void) | undefined} */
|
|
241
|
+
let writeSlogObject;
|
|
242
|
+
|
|
235
243
|
// this storagePort changes for every single message. We define it out here
|
|
236
244
|
// so the 'externalStorage' object can close over the single mutable
|
|
237
245
|
// instance, and we update the 'portNums.storage' value each time toSwingSet is called
|
|
238
246
|
async function launchAndInitializeSwingSet(bootMsg) {
|
|
247
|
+
const sendToChain = msg => chainSend(portNums.storage, msg);
|
|
239
248
|
// this object is used to store the mailbox state.
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const actionQueue = makeQueue(
|
|
253
|
-
makeChainStorage(
|
|
254
|
-
msg => chainSend(portNums.storage, msg),
|
|
255
|
-
`${STORAGE_PATH.ACTION_QUEUE}.`,
|
|
249
|
+
const fromBridgeMailbox = data => {
|
|
250
|
+
const ack = toNumber(data.ack);
|
|
251
|
+
const outbox = data.outbox.map(([seq, msg]) => [toNumber(seq), msg]);
|
|
252
|
+
return importMailbox({ outbox, ack });
|
|
253
|
+
};
|
|
254
|
+
const mailboxStorage = makeReadCachingStorage(
|
|
255
|
+
makePrefixedBridgeStorage(
|
|
256
|
+
sendToChain,
|
|
257
|
+
`${STORAGE_PATH.MAILBOX}.`,
|
|
258
|
+
'legacySet',
|
|
259
|
+
val => fromBridgeMailbox(JSON.parse(val)),
|
|
260
|
+
val => stringify(exportMailbox(val)),
|
|
256
261
|
),
|
|
257
262
|
);
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
const makeQueueStorage = queuePath => {
|
|
264
|
+
const { kvStore, commit, abort } = makeBufferedStorage(
|
|
265
|
+
makePrefixedBridgeStorage(
|
|
266
|
+
sendToChain,
|
|
267
|
+
`${queuePath}.`,
|
|
268
|
+
'setWithoutNotify',
|
|
269
|
+
x => x,
|
|
270
|
+
x => x,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
return harden({ ...kvStore, commit, abort });
|
|
274
|
+
};
|
|
275
|
+
const actionQueueStorage = makeQueueStorage(STORAGE_PATH.ACTION_QUEUE);
|
|
276
|
+
const highPriorityQueueStorage = makeQueueStorage(
|
|
277
|
+
STORAGE_PATH.HIGH_PRIORITY_QUEUE,
|
|
278
|
+
);
|
|
279
|
+
/**
|
|
280
|
+
* Callback invoked during SwingSet execution when new "export data" is
|
|
281
|
+
* generated by swingStore to be saved in the host's verified DB. In our
|
|
282
|
+
* case, we publish these entries in vstorage under a dedicated prefix.
|
|
283
|
+
* This effectively shadows the "export data" of the swingStore so that
|
|
284
|
+
* processes like state-sync can generate a verified "root of trust" to
|
|
285
|
+
* restore SwingSet state.
|
|
286
|
+
*
|
|
287
|
+
* @param {ReadonlyArray<[path: string, value?: string | null]>} updates
|
|
288
|
+
*/
|
|
289
|
+
const swingStoreExportCallback = async updates => {
|
|
290
|
+
// Allow I/O to proceed first
|
|
291
|
+
await waitUntilQuiescent();
|
|
292
|
+
|
|
293
|
+
const entries = updates.map(([key, value]) => {
|
|
294
|
+
if (typeof key !== 'string') {
|
|
295
|
+
throw Fail`Unexpected swingStore exported key ${q(key)}`;
|
|
296
|
+
}
|
|
297
|
+
const path = `${STORAGE_PATH.SWING_STORE}.${key}`;
|
|
298
|
+
if (value == null) {
|
|
299
|
+
return [path];
|
|
300
|
+
}
|
|
301
|
+
if (typeof value !== 'string') {
|
|
302
|
+
throw Fail`Unexpected ${typeof value} value for swingStore exported key ${q(
|
|
303
|
+
key,
|
|
304
|
+
)}`;
|
|
305
|
+
}
|
|
306
|
+
return [path, value];
|
|
263
307
|
});
|
|
264
|
-
|
|
265
|
-
|
|
308
|
+
sendToChain(
|
|
309
|
+
stringify({
|
|
310
|
+
method: 'setWithoutNotify',
|
|
311
|
+
args: entries,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
};
|
|
266
315
|
function doOutboundBridge(dstID, msg) {
|
|
267
316
|
const portNum = portNums[dstID];
|
|
268
317
|
if (portNum === undefined) {
|
|
@@ -278,7 +327,7 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
278
327
|
try {
|
|
279
328
|
return JSON.parse(respStr);
|
|
280
329
|
} catch (e) {
|
|
281
|
-
|
|
330
|
+
throw Fail`cannot JSON.parse(${JSON.stringify(respStr)}): ${e}`;
|
|
282
331
|
}
|
|
283
332
|
}
|
|
284
333
|
|
|
@@ -289,27 +338,32 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
289
338
|
const makeInstallationPublisher = () => {
|
|
290
339
|
const installationStorageNode = makeChainStorageRoot(
|
|
291
340
|
toStorage,
|
|
292
|
-
'swingset',
|
|
293
341
|
STORAGE_PATH.BUNDLES,
|
|
294
342
|
{ sequence: true },
|
|
295
343
|
);
|
|
296
|
-
const marshaller = makeMarshal(
|
|
297
|
-
|
|
298
|
-
|
|
344
|
+
const marshaller = makeMarshal(undefined, undefined, {
|
|
345
|
+
serializeBodyFormat: 'smallcaps',
|
|
346
|
+
});
|
|
347
|
+
const publish = makeSerializeToStorage(
|
|
348
|
+
installationStorageNode,
|
|
349
|
+
marshaller,
|
|
350
|
+
);
|
|
351
|
+
const publisher = harden({ publish });
|
|
299
352
|
return publisher;
|
|
300
353
|
};
|
|
301
354
|
|
|
302
355
|
const argv = {
|
|
303
|
-
ROLE: 'chain',
|
|
304
356
|
bootMsg,
|
|
305
357
|
};
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
358
|
+
const vatHref = await importMetaResolve(
|
|
359
|
+
env.CHAIN_BOOTSTRAP_VAT_CONFIG ||
|
|
360
|
+
argv.bootMsg.params.bootstrap_vat_config,
|
|
361
|
+
import.meta.url,
|
|
362
|
+
);
|
|
363
|
+
const vatconfig = new URL(vatHref).pathname;
|
|
364
|
+
|
|
365
|
+
// Delay makeShutdown to override the golang interrupts
|
|
366
|
+
const { registerShutdown } = makeShutdown();
|
|
313
367
|
|
|
314
368
|
const { metricsProvider } = getTelemetryProviders({
|
|
315
369
|
console,
|
|
@@ -317,37 +371,18 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
317
371
|
serviceName: TELEMETRY_SERVICE_NAME,
|
|
318
372
|
});
|
|
319
373
|
|
|
320
|
-
const {
|
|
321
|
-
LMDB_MAP_SIZE,
|
|
322
|
-
SWING_STORE_TRACE,
|
|
323
|
-
XSNAP_KEEP_SNAPSHOTS,
|
|
324
|
-
NODE_HEAP_SNAPSHOTS = -1,
|
|
325
|
-
} = env;
|
|
374
|
+
const { XSNAP_KEEP_SNAPSHOTS, NODE_HEAP_SNAPSHOTS = -1 } = env;
|
|
326
375
|
const slogSender = await makeSlogSender({
|
|
327
376
|
stateDir: stateDBDir,
|
|
328
377
|
env,
|
|
329
378
|
serviceName: TELEMETRY_SERVICE_NAME,
|
|
330
379
|
});
|
|
331
380
|
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
case '0':
|
|
338
|
-
case 'false':
|
|
339
|
-
break;
|
|
340
|
-
case '1':
|
|
341
|
-
case 'true':
|
|
342
|
-
swingStoreTraceFile = defaultTraceFile;
|
|
343
|
-
break;
|
|
344
|
-
default:
|
|
345
|
-
if (SWING_STORE_TRACE) {
|
|
346
|
-
swingStoreTraceFile = path.resolve(SWING_STORE_TRACE);
|
|
347
|
-
} else if (getFlagValue('trace-store')) {
|
|
348
|
-
swingStoreTraceFile = defaultTraceFile;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
381
|
+
const swingStoreTraceFile = processValue.getPath({
|
|
382
|
+
envName: 'SWING_STORE_TRACE',
|
|
383
|
+
flagName: 'trace-store',
|
|
384
|
+
trueValue: pathResolve(stateDBDir, 'store-trace.log'),
|
|
385
|
+
});
|
|
351
386
|
|
|
352
387
|
const keepSnapshots =
|
|
353
388
|
XSNAP_KEEP_SNAPSHOTS === '1' || XSNAP_KEEP_SNAPSHOTS === 'true';
|
|
@@ -356,67 +391,73 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
356
391
|
|
|
357
392
|
let lastCommitTime = 0;
|
|
358
393
|
let commitCallsSinceLastSnapshot = NaN;
|
|
359
|
-
const snapshotBaseDir =
|
|
394
|
+
const snapshotBaseDir = pathResolve(stateDBDir, 'node-heap-snapshots');
|
|
360
395
|
|
|
361
396
|
if (nodeHeapSnapshots >= 0) {
|
|
362
397
|
fs.mkdirSync(snapshotBaseDir, { recursive: true });
|
|
363
398
|
}
|
|
364
399
|
|
|
365
400
|
const afterCommitCallback = async () => {
|
|
401
|
+
const slogSenderFlushed = tryFlushSlogSender(slogSender, {
|
|
402
|
+
env,
|
|
403
|
+
log: console.warn,
|
|
404
|
+
});
|
|
405
|
+
|
|
366
406
|
// delay until all current promise reactions are drained so we can be sure
|
|
367
407
|
// that the commit-block reply has been sent to agcc through replier.resolve
|
|
368
|
-
await waitUntilQuiescent();
|
|
408
|
+
await Promise.all([slogSenderFlushed, waitUntilQuiescent()]);
|
|
369
409
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
410
|
+
try {
|
|
411
|
+
let heapSnapshot;
|
|
412
|
+
let heapSnapshotTime;
|
|
413
|
+
|
|
414
|
+
const t0 = performance.now();
|
|
415
|
+
engineGC();
|
|
416
|
+
const t1 = performance.now();
|
|
417
|
+
const memoryUsage = process.memoryUsage();
|
|
418
|
+
const t2 = performance.now();
|
|
419
|
+
const heapStats = v8.getHeapStatistics();
|
|
420
|
+
const t3 = performance.now();
|
|
421
|
+
|
|
422
|
+
commitCallsSinceLastSnapshot += 1;
|
|
423
|
+
if (
|
|
424
|
+
(nodeHeapSnapshots >= 0 && t0 - lastCommitTime > 30 * 1000) ||
|
|
425
|
+
(nodeHeapSnapshots > 0 &&
|
|
426
|
+
!(nodeHeapSnapshots > commitCallsSinceLastSnapshot))
|
|
427
|
+
) {
|
|
428
|
+
commitCallsSinceLastSnapshot = 0;
|
|
429
|
+
heapSnapshot = `Heap-${process.pid}-${Date.now()}.heapsnapshot`;
|
|
430
|
+
const snapshotPath = pathResolve(snapshotBaseDir, heapSnapshot);
|
|
431
|
+
v8.writeHeapSnapshot(snapshotPath);
|
|
432
|
+
heapSnapshotTime = performance.now() - t3;
|
|
433
|
+
}
|
|
434
|
+
lastCommitTime = t0;
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
memoryUsage,
|
|
438
|
+
heapStats,
|
|
439
|
+
heapSnapshot,
|
|
440
|
+
statsTime: {
|
|
441
|
+
forcedGc: t1 - t0,
|
|
442
|
+
memoryUsage: t2 - t1,
|
|
443
|
+
heapStats: t3 - t2,
|
|
444
|
+
heapSnapshot: heapSnapshotTime,
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
} catch (err) {
|
|
448
|
+
console.warn('Failed to gather memory stats', err);
|
|
449
|
+
return {};
|
|
392
450
|
}
|
|
393
|
-
lastCommitTime = t0;
|
|
394
|
-
|
|
395
|
-
await Promise.resolve(slogSender?.forceFlush?.()).catch(err => {
|
|
396
|
-
console.warn('Failed to flush slog sender', err);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
memoryUsage,
|
|
401
|
-
heapStats,
|
|
402
|
-
heapSnapshot,
|
|
403
|
-
statsTime: {
|
|
404
|
-
forcedGc: t1 - t0,
|
|
405
|
-
memoryUsage: t2 - t1,
|
|
406
|
-
heapStats: t3 - t2,
|
|
407
|
-
heapSnapshot: heapSnapshotTime,
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
451
|
};
|
|
411
452
|
|
|
412
453
|
const s = await launch({
|
|
413
|
-
|
|
454
|
+
actionQueueStorage,
|
|
455
|
+
highPriorityQueueStorage,
|
|
414
456
|
kernelStateDBDir: stateDBDir,
|
|
415
457
|
makeInstallationPublisher,
|
|
416
458
|
mailboxStorage,
|
|
417
459
|
clearChainSends,
|
|
418
460
|
replayChainSends,
|
|
419
|
-
setActivityhash,
|
|
420
461
|
bridgeOutbound: doOutboundBridge,
|
|
421
462
|
vatconfig,
|
|
422
463
|
argv,
|
|
@@ -424,18 +465,151 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
424
465
|
verboseBlocks: true,
|
|
425
466
|
metricsProvider,
|
|
426
467
|
slogSender,
|
|
427
|
-
|
|
468
|
+
swingStoreExportCallback,
|
|
428
469
|
swingStoreTraceFile,
|
|
429
470
|
keepSnapshots,
|
|
430
471
|
afterCommitCallback,
|
|
431
472
|
});
|
|
432
473
|
|
|
433
|
-
|
|
474
|
+
const { blockingSend, shutdown } = s;
|
|
475
|
+
({ writeSlogObject, savedChainSends } = s);
|
|
476
|
+
|
|
477
|
+
let pendingBlockingSend = Promise.resolve();
|
|
434
478
|
|
|
435
|
-
|
|
479
|
+
registerShutdown(async interrupted =>
|
|
480
|
+
Promise.all([
|
|
481
|
+
interrupted && pendingBlockingSend.then(shutdown),
|
|
482
|
+
discardStateSyncExport(),
|
|
483
|
+
]).then(() => {}),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return async action => {
|
|
487
|
+
const result = blockingSend(action);
|
|
488
|
+
pendingBlockingSend = Promise.resolve(result).then(
|
|
489
|
+
() => {},
|
|
490
|
+
() => {},
|
|
491
|
+
);
|
|
492
|
+
return result;
|
|
493
|
+
};
|
|
436
494
|
}
|
|
437
495
|
|
|
496
|
+
async function handleCosmosSnapshot(blockHeight, request, requestArgs) {
|
|
497
|
+
switch (request) {
|
|
498
|
+
case 'restore': {
|
|
499
|
+
const exportDir = requestArgs[0];
|
|
500
|
+
if (typeof exportDir !== 'string') {
|
|
501
|
+
throw Fail`Invalid exportDir argument ${q(exportDir)}`;
|
|
502
|
+
}
|
|
503
|
+
console.info(
|
|
504
|
+
'Restoring SwingSet state from snapshot at block height',
|
|
505
|
+
blockHeight,
|
|
506
|
+
);
|
|
507
|
+
return performStateSyncImport(
|
|
508
|
+
{ exportDir, stateDir: stateDBDir, blockHeight },
|
|
509
|
+
{ fs: { ...fs, ...fsPromises }, pathResolve, log: null },
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
case 'initiate': {
|
|
513
|
+
!stateSyncExport ||
|
|
514
|
+
Fail`Snapshot already in progress for ${stateSyncExport.blockHeight}`;
|
|
515
|
+
|
|
516
|
+
const exportData =
|
|
517
|
+
/** @type {Required<NonNullable<typeof stateSyncExport>>} */ ({
|
|
518
|
+
blockHeight,
|
|
519
|
+
});
|
|
520
|
+
stateSyncExport = exportData;
|
|
521
|
+
|
|
522
|
+
// eslint-disable-next-line @jessie.js/no-nested-await
|
|
523
|
+
await new Promise((resolve, reject) => {
|
|
524
|
+
tmpfs.dir(
|
|
525
|
+
{
|
|
526
|
+
prefix: `agd-state-sync-${blockHeight}-`,
|
|
527
|
+
unsafeCleanup: true,
|
|
528
|
+
},
|
|
529
|
+
(err, exportDir, cleanup) => {
|
|
530
|
+
if (err) {
|
|
531
|
+
reject(err);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
exportData.exportDir = exportDir;
|
|
535
|
+
/** @type {Promise<void> | undefined} */
|
|
536
|
+
let cleanupResult;
|
|
537
|
+
exportData.cleanup = async () => {
|
|
538
|
+
cleanupResult ||= new Promise(cleanupDone => {
|
|
539
|
+
// If the exporter is still the same, then the retriever
|
|
540
|
+
// is in charge of cleanups
|
|
541
|
+
if (stateSyncExport !== exportData) {
|
|
542
|
+
// @ts-expect-error wrong type definitions
|
|
543
|
+
cleanup(cleanupDone);
|
|
544
|
+
} else {
|
|
545
|
+
console.warn('unexpected call of state-sync cleanup');
|
|
546
|
+
cleanupDone();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
await cleanupResult;
|
|
550
|
+
};
|
|
551
|
+
resolve(null);
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
console.info(
|
|
557
|
+
'Initiating SwingSet state snapshot at block height',
|
|
558
|
+
blockHeight,
|
|
559
|
+
);
|
|
560
|
+
exportData.exporter = spawnSwingStoreExport(
|
|
561
|
+
{
|
|
562
|
+
stateDir: stateDBDir,
|
|
563
|
+
exportDir: exportData.exportDir,
|
|
564
|
+
blockHeight,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
fork,
|
|
568
|
+
},
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
exportData.exporter.onDone().catch(() => {
|
|
572
|
+
if (exportData === stateSyncExport) {
|
|
573
|
+
stateSyncExport = undefined;
|
|
574
|
+
}
|
|
575
|
+
return exportData.cleanup();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return exportData.exporter.onStarted().catch(err => {
|
|
579
|
+
console.warn(
|
|
580
|
+
`State-sync export failed for block ${blockHeight}`,
|
|
581
|
+
err,
|
|
582
|
+
);
|
|
583
|
+
throw err;
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
case 'discard': {
|
|
587
|
+
return discardStateSyncExport();
|
|
588
|
+
}
|
|
589
|
+
case 'retrieve': {
|
|
590
|
+
const exportData = stateSyncExport;
|
|
591
|
+
if (!exportData || !exportData.exporter) {
|
|
592
|
+
throw Fail`No snapshot in progress`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return exportData.exporter.onDone().then(() => {
|
|
596
|
+
if (exportData === stateSyncExport) {
|
|
597
|
+
// Don't cleanup, cosmos is now in charge
|
|
598
|
+
stateSyncExport = undefined;
|
|
599
|
+
return exportData.exportDir;
|
|
600
|
+
} else {
|
|
601
|
+
throw Fail`Snapshot was discarded`;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
default:
|
|
606
|
+
throw Fail`Unknown cosmos snapshot request ${request}`;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @type {Awaited<ReturnType<typeof launch>>['blockingSend'] | undefined} */
|
|
438
611
|
let blockingSend;
|
|
612
|
+
|
|
439
613
|
async function toSwingSet(action, _replier) {
|
|
440
614
|
// console.log(`toSwingSet`, action);
|
|
441
615
|
if (action.vibcPort) {
|
|
@@ -456,16 +630,51 @@ export default async function main(progname, args, { env, homedir, agcc }) {
|
|
|
456
630
|
portNums.lien = action.lienPort;
|
|
457
631
|
}
|
|
458
632
|
|
|
459
|
-
//
|
|
460
|
-
if (
|
|
461
|
-
|
|
633
|
+
// Snapshot actions are specific to cosmos chains and handled here
|
|
634
|
+
if (action.type === ActionType.COSMOS_SNAPSHOT) {
|
|
635
|
+
const { blockHeight, request, args: requestArgs } = action;
|
|
636
|
+
writeSlogObject?.({
|
|
637
|
+
type: 'cosmic-swingset-snapshot-start',
|
|
638
|
+
blockHeight,
|
|
639
|
+
request,
|
|
640
|
+
args: requestArgs,
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const resultP = handleCosmosSnapshot(blockHeight, request, requestArgs);
|
|
644
|
+
|
|
645
|
+
resultP.then(
|
|
646
|
+
result => {
|
|
647
|
+
writeSlogObject?.({
|
|
648
|
+
type: 'cosmic-swingset-snapshot-finish',
|
|
649
|
+
blockHeight,
|
|
650
|
+
request,
|
|
651
|
+
args: requestArgs,
|
|
652
|
+
result,
|
|
653
|
+
});
|
|
654
|
+
},
|
|
655
|
+
error => {
|
|
656
|
+
writeSlogObject?.({
|
|
657
|
+
type: 'cosmic-swingset-snapshot-finish',
|
|
658
|
+
blockHeight,
|
|
659
|
+
request,
|
|
660
|
+
args: requestArgs,
|
|
661
|
+
error,
|
|
662
|
+
});
|
|
663
|
+
},
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
return resultP;
|
|
462
667
|
}
|
|
463
668
|
|
|
669
|
+
// Ensure that initialization has completed.
|
|
670
|
+
blockingSend = await (blockingSend || launchAndInitializeSwingSet(action));
|
|
671
|
+
|
|
464
672
|
if (action.type === AG_COSMOS_INIT) {
|
|
465
673
|
// console.error('got AG_COSMOS_INIT', action);
|
|
466
674
|
return true;
|
|
467
675
|
}
|
|
468
676
|
|
|
677
|
+
// Block related actions are processed by `blockingSend`
|
|
469
678
|
return blockingSend(action);
|
|
470
679
|
}
|
|
471
680
|
}
|