@agoric/swingset-vat 0.33.0-u17.1 → 0.33.0-u18.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/swingset-vat",
3
- "version": "0.33.0-u17.1",
3
+ "version": "0.33.0-u18.1",
4
4
  "description": "Vat/Container Launcher",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,34 +27,33 @@
27
27
  "@types/yargs-parser": "^21.0.0"
28
28
  },
29
29
  "dependencies": {
30
- "@agoric/internal": "^0.4.0-u17.1",
31
- "@agoric/kmarshal": "^0.1.1-u17.1",
32
- "@agoric/store": "^0.9.3-u17.1",
33
- "@agoric/swing-store": "^0.9.2-u17.1",
34
- "@agoric/swingset-liveslots": "^0.10.3-u17.1",
35
- "@agoric/swingset-xsnap-supervisor": "^0.10.3-u17.1",
36
- "@agoric/time": "^0.3.3-u17.1",
37
- "@agoric/vat-data": "^0.5.3-u17.1",
38
- "@agoric/xsnap": "^0.14.3-u17.1",
39
- "@agoric/xsnap-lockdown": "^0.14.1-u17.1",
40
- "@endo/base64": "^1.0.7",
41
- "@endo/bundle-source": "^3.4.0",
42
- "@endo/captp": "^4.3.0",
43
- "@endo/check-bundle": "^1.0.9",
44
- "@endo/compartment-mapper": "^1.2.2",
45
- "@endo/errors": "^1.2.5",
46
- "@endo/eventual-send": "^1.2.5",
47
- "@endo/far": "^1.1.5",
48
- "@endo/import-bundle": "^1.2.2",
49
- "@endo/init": "^1.1.4",
50
- "@endo/marshal": "^1.5.3",
51
- "@endo/nat": "^5.0.10",
52
- "@endo/pass-style": "^1.4.3",
53
- "@endo/patterns": "^1.4.3",
54
- "@endo/promise-kit": "^1.1.5",
55
- "@endo/ses-ava": "^1.2.5",
56
- "@endo/stream": "^1.2.5",
57
- "@endo/zip": "^1.0.7",
30
+ "@agoric/internal": "^0.4.0-u18.1",
31
+ "@agoric/kmarshal": "^0.1.1-u18.1",
32
+ "@agoric/store": "^0.9.3-u18.1",
33
+ "@agoric/swing-store": "^0.10.0-u18.1",
34
+ "@agoric/swingset-liveslots": "^0.10.3-u18.1",
35
+ "@agoric/swingset-xsnap-supervisor": "^0.10.3-u18.1",
36
+ "@agoric/time": "^0.3.3-u18.1",
37
+ "@agoric/vat-data": "^0.5.3-u18.1",
38
+ "@agoric/xsnap-lockdown": "^0.14.1-u18.1",
39
+ "@endo/base64": "^1.0.9",
40
+ "@endo/bundle-source": "^3.5.0",
41
+ "@endo/captp": "^4.4.3",
42
+ "@endo/check-bundle": "^1.0.12",
43
+ "@endo/compartment-mapper": "^1.4.0",
44
+ "@endo/errors": "^1.2.8",
45
+ "@endo/eventual-send": "^1.2.8",
46
+ "@endo/far": "^1.1.9",
47
+ "@endo/import-bundle": "^1.3.2",
48
+ "@endo/init": "^1.1.7",
49
+ "@endo/marshal": "^1.6.2",
50
+ "@endo/nat": "^5.0.13",
51
+ "@endo/pass-style": "^1.4.7",
52
+ "@endo/patterns": "^1.4.7",
53
+ "@endo/promise-kit": "^1.1.8",
54
+ "@endo/ses-ava": "^1.2.8",
55
+ "@endo/stream": "^1.2.8",
56
+ "@endo/zip": "^1.0.9",
58
57
  "ansi-styles": "^6.2.1",
59
58
  "anylogger": "^0.21.0",
60
59
  "better-sqlite3": "^9.1.1",
@@ -65,6 +64,7 @@
65
64
  "yargs-parser": "^21.1.1"
66
65
  },
67
66
  "peerDependencies": {
67
+ "@agoric/xsnap": "^0.14.2",
68
68
  "ava": "^5.3.0"
69
69
  },
70
70
  "files": [
@@ -101,7 +101,7 @@
101
101
  "access": "public"
102
102
  },
103
103
  "typeCoverage": {
104
- "atLeast": 75.7
104
+ "atLeast": 76.28
105
105
  },
106
- "gitHead": "5259430561693bfcf58516c3ea54123895859708"
106
+ "gitHead": "f8c45b8a2e29a51522a81a6692af25b2d7f6b50f"
107
107
  }
@@ -15,6 +15,7 @@ import { initSwingStore } from '@agoric/swing-store';
15
15
 
16
16
  import { mustMatch, M } from '@endo/patterns';
17
17
  import { checkBundle } from '@endo/check-bundle/lite.js';
18
+ import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
18
19
  import engineGC from '@agoric/internal/src/lib-nodejs/engine-gc.js';
19
20
  import { startSubprocessWorker } from '@agoric/internal/src/lib-nodejs/spawnSubprocessWorker.js';
20
21
  import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js';
@@ -263,13 +264,6 @@ export async function makeSwingsetController(
263
264
 
264
265
  await kernel.start();
265
266
 
266
- /**
267
- * @param {T} x
268
- * @returns {T}
269
- * @template T
270
- */
271
- const defensiveCopy = x => JSON.parse(JSON.stringify(x));
272
-
273
267
  /**
274
268
  * Validate and install a code bundle.
275
269
  *
@@ -304,7 +298,7 @@ export async function makeSwingsetController(
304
298
  writeSlogObject,
305
299
 
306
300
  dump() {
307
- return defensiveCopy(kernel.dump());
301
+ return deepCopyJsonable(kernel.dump());
308
302
  },
309
303
 
310
304
  verboseDebugMode(flag) {
@@ -340,11 +334,11 @@ export async function makeSwingsetController(
340
334
  },
341
335
 
342
336
  getStats() {
343
- return defensiveCopy(kernel.getStats());
337
+ return deepCopyJsonable(kernel.getStats());
344
338
  },
345
339
 
346
340
  getStatus() {
347
- return defensiveCopy(kernel.getStatus());
341
+ return deepCopyJsonable(kernel.getStatus());
348
342
  },
349
343
 
350
344
  getActivityhash() {
@@ -366,6 +360,10 @@ export async function makeSwingsetController(
366
360
  return kref;
367
361
  },
368
362
 
363
+ kpRegisterInterest(kpid) {
364
+ return kernel.kpRegisterInterest(kpid);
365
+ },
366
+
369
367
  kpStatus(kpid) {
370
368
  return kernel.kpStatus(kpid);
371
369
  },
@@ -384,6 +382,8 @@ export async function makeSwingsetController(
384
382
  return kernel.deviceNameToID(deviceName);
385
383
  },
386
384
 
385
+ injectQueuedUpgradeEvents: () => kernel.injectQueuedUpgradeEvents(),
386
+
387
387
  /**
388
388
  * Queue a method call into the named vat
389
389
  *
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-use-before-define */
2
-
3
1
  import { assert, Fail } from '@endo/errors';
4
2
  import { makeMarshal } from '@endo/marshal';
5
3
  import { Far } from '@endo/far';
@@ -1,9 +1,9 @@
1
- /* global process */
1
+ /* eslint-env node */
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
4
 
5
- import { assert, Fail } from '@endo/errors';
6
- import { makeTracer } from '@agoric/internal';
5
+ import { assert, b, Fail } from '@endo/errors';
6
+ import { deepCopyJsonable, makeTracer } from '@agoric/internal';
7
7
  import { mustMatch } from '@agoric/store';
8
8
  import bundleSource from '@endo/bundle-source';
9
9
  import { resolve as resolveModuleSpecifier } from 'import-meta-resolve';
@@ -86,16 +86,6 @@ export async function buildKernelBundles() {
86
86
  return harden({ kernel: kernelBundle, ...vdBundles });
87
87
  }
88
88
 
89
- function byName(a, b) {
90
- if (a.name < b.name) {
91
- return -1;
92
- }
93
- if (a.name > b.name) {
94
- return 1;
95
- }
96
- return 0;
97
- }
98
-
99
89
  /**
100
90
  * Scan a directory for files defining the vats to bootstrap for a swingset, and
101
91
  * produce a swingset config object for what was found there. Looks for files
@@ -126,18 +116,18 @@ export function loadBasedir(basedir, options = {}) {
126
116
  const { includeDevDependencies = false, bundleFormat = undefined } = options;
127
117
  /** @type { SwingSetConfigDescriptor } */
128
118
  const vats = {};
129
- const subs = fs.readdirSync(basedir, { withFileTypes: true });
130
- subs.sort(byName);
131
- for (const dirent of subs) {
132
- if (
133
- dirent.name.startsWith('vat-') &&
134
- dirent.name.endsWith('.js') &&
135
- dirent.isFile()
136
- ) {
137
- const name = dirent.name.slice('vat-'.length, -'.js'.length);
138
- const vatSourcePath = path.resolve(basedir, dirent.name);
139
- vats[name] = { sourceSpec: vatSourcePath, parameters: {} };
140
- }
119
+ const rVatName = /^vat-(.*)\.js$/s;
120
+ const files = fs.readdirSync(basedir, { withFileTypes: true });
121
+ const vatFiles = files.flatMap(dirent => {
122
+ const file = dirent.name;
123
+ const m = rVatName.exec(file);
124
+ return m && dirent.isFile() ? [{ file, label: m[1] }] : [];
125
+ });
126
+ // eslint-disable-next-line no-shadow,no-nested-ternary
127
+ vatFiles.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
128
+ for (const { file, label } of vatFiles) {
129
+ const vatSourcePath = path.resolve(basedir, file);
130
+ vats[label] = { sourceSpec: vatSourcePath, parameters: {} };
141
131
  }
142
132
  /** @type {string | void} */
143
133
  let bootstrapPath = path.resolve(basedir, 'bootstrap.js');
@@ -185,37 +175,42 @@ async function resolveSpecFromConfig(referrer, specPath) {
185
175
  }
186
176
 
187
177
  /**
188
- * For each entry in a config descriptor (i.e, `vats`, `bundles`, etc), convert
189
- * it to normal form: resolve each pathname to a context-insensitive absolute
190
- * path and make sure it has a `parameters` property if it's supposed to.
178
+ * Convert each entry in a config descriptor group (`vats`/`bundles`/etc.) to
179
+ * normal form: resolve each pathname to a context-insensitive absolute path and
180
+ * run any other appropriate fixup.
191
181
  *
192
- * @param {SwingSetConfigDescriptor | void} desc The config descriptor to be normalized.
193
- * @param {string} referrer The pathname of the file or directory in which the
194
- * config file was found
195
- * @param {boolean} expectParameters `true` if the entries should have parameters (for
196
- * example, `true` for `vats` but `false` for bundles).
182
+ * @param {SwingSetConfig} config
183
+ * @param {'vats' | 'bundles' | 'devices'} groupName
184
+ * @param {string | undefined} configPath of the containing config file
185
+ * @param {string} referrer URL
186
+ * @param {(entry: SwingSetConfigProperties, name?: string) => void} [fixupEntry]
187
+ * A function to call on each entry to e.g. add defaults for missing fields
188
+ * such as vat `parameters`.
197
189
  */
198
- async function normalizeConfigDescriptor(desc, referrer, expectParameters) {
199
- const normalizeSpec = async (entry, key) => {
200
- return resolveSpecFromConfig(referrer, entry[key]).then(spec => {
201
- fs.existsSync(spec) ||
202
- Fail`spec for ${entry[key]} does not exist: ${spec}`;
203
- entry[key] = spec;
204
- });
190
+ async function normalizeConfigDescriptor(
191
+ config,
192
+ groupName,
193
+ configPath,
194
+ referrer,
195
+ fixupEntry,
196
+ ) {
197
+ const normalizeSpec = async (entry, specKey, name) => {
198
+ const sourcePath = await resolveSpecFromConfig(referrer, entry[specKey]);
199
+ fs.existsSync(sourcePath) ||
200
+ Fail`${sourcePath} for ${b(groupName)}[${name}].${b(specKey)} in ${configPath} config file does not exist`;
201
+ entry[specKey] = sourcePath;
205
202
  };
206
203
 
207
204
  const jobs = [];
205
+ const desc = config[groupName];
208
206
  if (desc) {
209
- for (const name of Object.keys(desc)) {
210
- const entry = desc[name];
207
+ for (const [name, entry] of Object.entries(desc)) {
208
+ fixupEntry?.(entry, name);
211
209
  if ('sourceSpec' in entry) {
212
- jobs.push(normalizeSpec(entry, 'sourceSpec'));
210
+ jobs.push(normalizeSpec(entry, 'sourceSpec', name));
213
211
  }
214
212
  if ('bundleSpec' in entry) {
215
- jobs.push(normalizeSpec(entry, 'bundleSpec'));
216
- }
217
- if (expectParameters && !entry.parameters) {
218
- entry.parameters = {};
213
+ jobs.push(normalizeSpec(entry, 'bundleSpec', name));
219
214
  }
220
215
  }
221
216
  }
@@ -223,27 +218,41 @@ async function normalizeConfigDescriptor(desc, referrer, expectParameters) {
223
218
  }
224
219
 
225
220
  /**
226
- * Read and parse a swingset config file and return it in normalized form.
227
- *
228
- * @param {string} configPath Path to the config file to be processed
229
- *
230
- * @returns {Promise<SwingSetConfig | null>} the contained config object, in normalized form, or null if the
231
- * requested config file did not exist.
221
+ * @param {SwingSetConfig} config
222
+ * @param {string} [configPath]
223
+ * @returns {Promise<void>}
224
+ * @throws {Error} if the config is invalid
225
+ */
226
+ export async function normalizeConfig(config, configPath) {
227
+ const base = `file://${process.cwd()}/`;
228
+ const referrer = configPath
229
+ ? new URL(configPath, base).href
230
+ : new URL(base).href;
231
+ const fixupVat = vat => (vat.parameters ||= {});
232
+ await Promise.all([
233
+ normalizeConfigDescriptor(config, 'vats', configPath, referrer, fixupVat),
234
+ normalizeConfigDescriptor(config, 'bundles', configPath, referrer),
235
+ // TODO: represent devices
236
+ // normalizeConfigDescriptor(config, 'devices', configPath, referrer),
237
+ ]);
238
+ config.bootstrap ||
239
+ Fail`no designated bootstrap vat in ${configPath} config file`;
240
+ (config.vats && config.vats[/** @type {string} */ (config.bootstrap)]) ||
241
+ Fail`bootstrap vat ${config.bootstrap} not found in ${configPath} config file`;
242
+ }
243
+
244
+ /**
245
+ * Read and normalize a swingset config file.
232
246
  *
233
- * @throws {Error} if the file existed but was inaccessible, malformed, or otherwise
234
- * invalid.
247
+ * @param {string} configPath
248
+ * @returns {Promise<SwingSetConfig | null>} the normalized config,
249
+ * or null if the file did not exist
235
250
  */
236
251
  export async function loadSwingsetConfigFile(configPath) {
237
252
  await null;
238
253
  try {
239
254
  const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
240
- const referrer = new URL(configPath, `file://${process.cwd()}/`).toString();
241
- await normalizeConfigDescriptor(config.vats, referrer, true);
242
- await normalizeConfigDescriptor(config.bundles, referrer, false);
243
- // await normalizeConfigDescriptor(config.devices, referrer, true); // TODO: represent devices
244
- config.bootstrap || Fail`no designated bootstrap vat in ${configPath}`;
245
- (config.vats && config.vats[config.bootstrap]) ||
246
- Fail`bootstrap vat ${config.bootstrap} not found in ${configPath}`;
255
+ await normalizeConfig(config, configPath);
247
256
  return config;
248
257
  } catch (e) {
249
258
  console.error(`failed to load ${configPath}`);
@@ -314,7 +323,7 @@ export async function initializeSwingset(
314
323
  } = runtimeOptions;
315
324
 
316
325
  // copy config so we can safely mess with it even if it's shared or hardened
317
- config = JSON.parse(JSON.stringify(config));
326
+ config = deepCopyJsonable(config);
318
327
  if (!config.bundles) {
319
328
  config.bundles = {};
320
329
  }
@@ -1,14 +1,47 @@
1
+ import { Fail } from '@endo/errors';
1
2
  import {
2
3
  DEFAULT_REAP_DIRT_THRESHOLD_KEY,
3
4
  DEFAULT_GC_KREFS_PER_BOYD,
4
5
  getAllDynamicVats,
5
6
  getAllStaticVats,
7
+ incrementReferenceCount,
8
+ readQueue,
6
9
  } from '../kernel/state/kernelKeeper.js';
10
+ import { enumeratePrefixedKeys } from '../kernel/state/storageHelper.js';
7
11
 
8
- const upgradeVatV0toV1 = (kvStore, defaultReapDirtThreshold, vatID) => {
9
- // This is called, once per vat, when upgradeSwingset migrates from
10
- // v0 to v1
12
+ /**
13
+ * @import {ReapDirtThreshold, RunQueueEvent} from '../types-internal.js';
14
+ */
15
+
16
+ /**
17
+ * Parse a string of decimal digits into a number.
18
+ *
19
+ * @param {string} digits
20
+ * @param {string} label
21
+ * @returns {number}
22
+ */
23
+ const mustParseInt = (digits, label) => {
24
+ assert(
25
+ digits.match(/^\d+$/),
26
+ `expected ${label}=${digits} to be a decimal integer`,
27
+ );
28
+ return Number(digits);
29
+ };
11
30
 
31
+ /**
32
+ * Called for each vat when upgradeSwingset migrates from v0 to v1.
33
+ *
34
+ * @param {KVStore} kvStore
35
+ * @param {(key: string) => string} getRequired
36
+ * @param {ReapDirtThreshold} defaultReapDirtThreshold
37
+ * @param {string} vatID
38
+ */
39
+ const upgradeVatV0toV1 = (
40
+ kvStore,
41
+ getRequired,
42
+ defaultReapDirtThreshold,
43
+ vatID,
44
+ ) => {
12
45
  // schema v0:
13
46
  // Each vat has a `vNN.reapInterval` and `vNN.reapCountdown`.
14
47
  // vNN.options has a `.reapInterval` property (however it was not
@@ -25,15 +58,10 @@ const upgradeVatV0toV1 = (kvStore, defaultReapDirtThreshold, vatID) => {
25
58
  // `defaultReapDirtThreshold`)
26
59
 
27
60
  const reapDirtKey = `${vatID}.reapDirt`;
28
-
29
- assert(kvStore.has(oldReapIntervalKey), oldReapIntervalKey);
30
- assert(kvStore.has(oldReapCountdownKey), oldReapCountdownKey);
31
61
  assert(!kvStore.has(reapDirtKey), reapDirtKey);
32
62
 
33
- const reapIntervalString = kvStore.get(oldReapIntervalKey);
34
- const reapCountdownString = kvStore.get(oldReapCountdownKey);
35
- assert(reapIntervalString !== undefined);
36
- assert(reapCountdownString !== undefined);
63
+ const reapIntervalString = getRequired(oldReapIntervalKey);
64
+ const reapCountdownString = getRequired(oldReapCountdownKey);
37
65
 
38
66
  const intervalIsNever = reapIntervalString === 'never';
39
67
  const countdownIsNever = reapCountdownString === 'never';
@@ -54,8 +82,11 @@ const upgradeVatV0toV1 = (kvStore, defaultReapDirtThreshold, vatID) => {
54
82
  threshold.never = true;
55
83
  } else {
56
84
  // deduce delivery count from old countdown values
57
- const reapInterval = Number.parseInt(reapIntervalString, 10);
58
- const reapCountdown = Number.parseInt(reapCountdownString, 10);
85
+ const reapInterval = mustParseInt(reapIntervalString, oldReapIntervalKey);
86
+ const reapCountdown = mustParseInt(
87
+ reapCountdownString,
88
+ oldReapCountdownKey,
89
+ );
59
90
  const deliveries = reapInterval - reapCountdown;
60
91
  reapDirt.deliveries = Math.max(deliveries, 0); // just in case
61
92
  if (reapInterval !== defaultReapDirtThreshold.deliveries) {
@@ -68,7 +99,7 @@ const upgradeVatV0toV1 = (kvStore, defaultReapDirtThreshold, vatID) => {
68
99
  kvStore.set(reapDirtKey, JSON.stringify(reapDirt));
69
100
 
70
101
  // Update options to use the new schema.
71
- const options = JSON.parse(kvStore.get(vatOptionsKey));
102
+ const options = JSON.parse(getRequired(vatOptionsKey));
72
103
  delete options.reapInterval;
73
104
  options.reapDirtThreshold = threshold;
74
105
  kvStore.set(vatOptionsKey, JSON.stringify(options));
@@ -92,16 +123,15 @@ const upgradeVatV0toV1 = (kvStore, defaultReapDirtThreshold, vatID) => {
92
123
  * `hostStorage.commit()` afterwards.
93
124
  *
94
125
  * @param {SwingStoreKernelStorage} kernelStorage
95
- * @returns {boolean} true if any changes were made
126
+ * @returns {{ modified: boolean }}
96
127
  */
97
128
  export const upgradeSwingset = kernelStorage => {
98
129
  const { kvStore } = kernelStorage;
99
- let modified = false;
100
- let vstring = kvStore.get('version');
101
- if (vstring === undefined) {
102
- vstring = '0';
103
- }
104
- let version = Number(vstring);
130
+ /** @type {RunQueueEvent[]} */
131
+ const upgradeEvents = [];
132
+ const vstring = kvStore.get('version');
133
+ const version = Number(vstring) || 0;
134
+ let newVersion;
105
135
 
106
136
  /**
107
137
  * @param {string} key
@@ -156,11 +186,7 @@ export const upgradeSwingset = kernelStorage => {
156
186
  assert(kvStore.has(oldDefaultReapIntervalKey));
157
187
  assert(!kvStore.has(DEFAULT_REAP_DIRT_THRESHOLD_KEY));
158
188
 
159
- /**
160
- * @typedef { import('../types-internal.js').ReapDirtThreshold } ReapDirtThreshold
161
- */
162
-
163
- /** @type ReapDirtThreshold */
189
+ /** @type {ReapDirtThreshold} */
164
190
  const threshold = {
165
191
  deliveries: 'never',
166
192
  gcKrefs: 'never',
@@ -169,9 +195,7 @@ export const upgradeSwingset = kernelStorage => {
169
195
 
170
196
  const oldValue = getRequired(oldDefaultReapIntervalKey);
171
197
  if (oldValue !== 'never') {
172
- const value = Number.parseInt(oldValue, 10);
173
- assert.typeof(value, 'number');
174
- threshold.deliveries = value;
198
+ threshold.deliveries = mustParseInt(oldValue, oldDefaultReapIntervalKey);
175
199
  // if BOYD wasn't turned off entirely (eg
176
200
  // defaultReapInterval='never', which only happens in unit
177
201
  // tests), then pretend we wanted a gcKrefs= threshold all
@@ -186,27 +210,146 @@ export const upgradeSwingset = kernelStorage => {
186
210
 
187
211
  // now upgrade all vats
188
212
  for (const [_name, vatID] of getAllStaticVats(kvStore)) {
189
- upgradeVatV0toV1(kvStore, threshold, vatID);
213
+ upgradeVatV0toV1(kvStore, getRequired, threshold, vatID);
190
214
  }
191
215
  for (const vatID of getAllDynamicVats(getRequired)) {
192
- upgradeVatV0toV1(kvStore, threshold, vatID);
216
+ upgradeVatV0toV1(kvStore, getRequired, threshold, vatID);
193
217
  }
194
218
 
195
- modified = true;
196
- version = 1;
219
+ newVersion = 1;
197
220
  }
198
221
 
199
222
  if (version < 2) {
200
223
  // schema v2: add vats.terminated = []
201
224
  assert(!kvStore.has('vats.terminated'));
202
225
  kvStore.set('vats.terminated', JSON.stringify([]));
203
- modified = true;
204
- version = 2;
226
+ newVersion = 2;
227
+ }
228
+
229
+ if (version < 3) {
230
+ // v3 means that we've completed remediation for bug #9039
231
+ console.log(`Starting remediation of bug #9039`);
232
+
233
+ // find all terminated vats
234
+ const terminated = new Set(JSON.parse(getRequired('vats.terminated')));
235
+
236
+ // find all live vats
237
+ const allVatIDs = [];
238
+ for (const [_name, vatID] of getAllStaticVats(kvStore)) {
239
+ if (!terminated.has(vatID)) {
240
+ allVatIDs.push(vatID);
241
+ }
242
+ }
243
+ for (const vatID of getAllDynamicVats(getRequired)) {
244
+ if (!terminated.has(vatID)) {
245
+ allVatIDs.push(vatID);
246
+ }
247
+ }
248
+
249
+ // find all pending notifies
250
+ const notifies = new Map(); // .get(kpid) = [vatIDs..];
251
+ for (const name of ['runQueue', 'acceptanceQueue']) {
252
+ for (const rq of readQueue(name, getRequired)) {
253
+ if (rq.type === 'notify') {
254
+ const { vatID, kpid } = rq;
255
+ assert(vatID);
256
+ assert(kpid);
257
+ let vats = notifies.get(kpid);
258
+ if (!vats) {
259
+ vats = [];
260
+ notifies.set(kpid, vats);
261
+ }
262
+ vats.push(vatID);
263
+ }
264
+ }
265
+ }
266
+ console.log(` - pending notifies:`, notifies);
267
+
268
+ // cache of known-settled kpids: will grow to num(kpids)
269
+ const KPIDStatus = new Map();
270
+ const isSettled = kpid => {
271
+ if (KPIDStatus.has(kpid)) {
272
+ return KPIDStatus.get(kpid);
273
+ }
274
+ const state = kvStore.get(`${kpid}.state`);
275
+ // missing state means the kpid is deleted somehow, shouldn't happen
276
+ state || Fail`${kpid}.state is missing`;
277
+ const settled = state !== 'unresolved';
278
+ KPIDStatus.set(kpid, settled);
279
+ return settled;
280
+ };
281
+
282
+ // walk vNN.c.kpNN for all vats, for each one check the
283
+ // kpNN.state, for the settled ones check for a pending notify,
284
+ // record the ones without a pending notify
285
+
286
+ const buggyKPIDs = []; // [kpid, vatID]
287
+ for (const vatID of allVatIDs) {
288
+ const prefix = `${vatID}.c.`;
289
+ const len = prefix.length;
290
+ const ckpPrefix = `${vatID}.c.kp`;
291
+ for (const key of enumeratePrefixedKeys(kvStore, ckpPrefix)) {
292
+ const kpid = key.slice(len);
293
+ if (isSettled(kpid)) {
294
+ const n = notifies.get(kpid);
295
+ if (!n || !n.includes(vatID)) {
296
+ // there is no pending notify
297
+ buggyKPIDs.push([kpid, vatID]);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ console.log(` - found ${buggyKPIDs.length} buggy kpids, enqueueing fixes`);
303
+
304
+ // now fix it. The bug means we failed to delete the c-list entry
305
+ // and decref it back when the promise was rejected. That decref
306
+ // would have pushed the kpid onto maybeFreeKrefs, which would
307
+ // have triggered a refcount check at end-of-crank, which might
308
+ // have deleted the promise records (if nothing else was
309
+ // referencing the promise, like arguments in messages enqueued to
310
+ // unresolved promises, or something transient on the
311
+ // run-queue). Deleting those promise records might have decreffed
312
+ // krefs in the rejection data (although in general 9039 rejects
313
+ // those promises with non-slot-bearing DisconnectionObjects).
314
+ //
315
+ // To avoid duplicating a lot of kernel code inside this upgrade
316
+ // handler, we do the simplest possible thing: enqueue a notify to
317
+ // the upgraded vat for all these leftover promises. The new vat
318
+ // incarnation will ignore it (they don't recognize the vpid), but
319
+ // the dispatch.notify() delivery will clear the c-list and decref
320
+ // the kpid, and will trigger all the usual GC work. Note that
321
+ // these notifies will be delivered before any activity the host
322
+ // app might trigger for e.g. a chain upgrade, but they should not
323
+ // cause userspace-visible behavior (non-slot-bearing rejection
324
+ // data means no other vat will even get a gc-action delivery:
325
+ // only the upgraded vat will see anything, and those deliveries
326
+ // won't make it past liveslots).
327
+
328
+ let count = 0;
329
+ for (const [kpid, vatID] of buggyKPIDs) {
330
+ // account for the reference to this kpid in upgradeEvents
331
+ incrementReferenceCount(getRequired, kvStore, kpid, `enq|notify`);
332
+ upgradeEvents.push({ type: 'notify', vatID, kpid });
333
+ count += 1;
334
+ }
335
+
336
+ console.log(` - #9039 remediation complete, ${count} notifies to inject`);
337
+ newVersion = 3;
338
+ }
339
+
340
+ const modified = newVersion !== undefined;
341
+
342
+ if (upgradeEvents.length) {
343
+ assert(modified);
344
+ // stash until host calls controller.injectQueuedUpgradeEvents()
345
+ const oldEvents = JSON.parse(kvStore.get('upgradeEvents') || '[]');
346
+ const events = [...oldEvents, ...upgradeEvents];
347
+ kvStore.set('upgradeEvents', JSON.stringify(events));
205
348
  }
206
349
 
207
350
  if (modified) {
208
- kvStore.set('version', `${version}`);
351
+ kvStore.set('version', `${newVersion}`);
209
352
  }
210
- return modified;
353
+ return harden({ modified });
211
354
  };
212
355
  harden(upgradeSwingset);
@@ -1,5 +1,6 @@
1
1
  import { Fail } from '@endo/errors';
2
2
  import { Far } from '@endo/far';
3
+ import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
3
4
 
4
5
  function sanitize(data) {
5
6
  if (data === undefined) {
@@ -8,7 +9,7 @@ function sanitize(data) {
8
9
  if (data instanceof Error) {
9
10
  data = data.stack;
10
11
  }
11
- return JSON.parse(JSON.stringify(data));
12
+ return deepCopyJsonable(data);
12
13
  }
13
14
 
14
15
  /**
@@ -32,7 +33,7 @@ export function buildRootDeviceNode(tools) {
32
33
 
33
34
  function inboundCallback(...args) {
34
35
  inboundHandler || Fail`inboundHandler not yet registered`;
35
- const safeArgs = JSON.parse(JSON.stringify(args));
36
+ const safeArgs = deepCopyJsonable(args);
36
37
  try {
37
38
  SO(inboundHandler).inbound(...harden(safeArgs));
38
39
  } catch (e) {