diff --git a/packages/SwingSet/src/controller/controller.js b/packages/SwingSet/src/controller/controller.js index c3091989814..eace725d4ce 100644 --- a/packages/SwingSet/src/controller/controller.js +++ b/packages/SwingSet/src/controller/controller.js @@ -15,6 +15,7 @@ import { initSwingStore } from '@agoric/swing-store'; import { mustMatch, M } from '@endo/patterns'; import { checkBundle } from '@endo/check-bundle/lite.js'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; import engineGC from '@agoric/internal/src/lib-nodejs/engine-gc.js'; import { startSubprocessWorker } from '@agoric/internal/src/lib-nodejs/spawnSubprocessWorker.js'; import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js'; @@ -263,13 +264,6 @@ export async function makeSwingsetController( await kernel.start(); - /** - * @param {T} x - * @returns {T} - * @template T - */ - const defensiveCopy = x => JSON.parse(JSON.stringify(x)); - /** * Validate and install a code bundle. * @@ -304,7 +298,7 @@ export async function makeSwingsetController( writeSlogObject, dump() { - return defensiveCopy(kernel.dump()); + return deepCopyJsonable(kernel.dump()); }, verboseDebugMode(flag) { @@ -340,11 +334,11 @@ export async function makeSwingsetController( }, getStats() { - return defensiveCopy(kernel.getStats()); + return deepCopyJsonable(kernel.getStats()); }, getStatus() { - return defensiveCopy(kernel.getStatus()); + return deepCopyJsonable(kernel.getStatus()); }, getActivityhash() { diff --git a/packages/SwingSet/src/controller/initializeSwingset.js b/packages/SwingSet/src/controller/initializeSwingset.js index 3fb6f0a0768..7ecb1f048c3 100644 --- a/packages/SwingSet/src/controller/initializeSwingset.js +++ b/packages/SwingSet/src/controller/initializeSwingset.js @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import { assert, b, Fail } from '@endo/errors'; -import { makeTracer } from '@agoric/internal'; +import { deepCopyJsonable, makeTracer } from '@agoric/internal'; import { mustMatch } from '@agoric/store'; import bundleSource from '@endo/bundle-source'; import { resolve as resolveModuleSpecifier } from 'import-meta-resolve'; @@ -323,7 +323,7 @@ export async function initializeSwingset( } = runtimeOptions; // copy config so we can safely mess with it even if it's shared or hardened - config = JSON.parse(JSON.stringify(config)); + config = deepCopyJsonable(config); if (!config.bundles) { config.bundles = {}; } diff --git a/packages/SwingSet/src/devices/bridge/device-bridge.js b/packages/SwingSet/src/devices/bridge/device-bridge.js index 216bf6556ca..3cdd8710e24 100644 --- a/packages/SwingSet/src/devices/bridge/device-bridge.js +++ b/packages/SwingSet/src/devices/bridge/device-bridge.js @@ -1,5 +1,6 @@ import { Fail } from '@endo/errors'; import { Far } from '@endo/far'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; function sanitize(data) { if (data === undefined) { @@ -8,7 +9,7 @@ function sanitize(data) { if (data instanceof Error) { data = data.stack; } - return JSON.parse(JSON.stringify(data)); + return deepCopyJsonable(data); } /** @@ -32,7 +33,7 @@ export function buildRootDeviceNode(tools) { function inboundCallback(...args) { inboundHandler || Fail`inboundHandler not yet registered`; - const safeArgs = JSON.parse(JSON.stringify(args)); + const safeArgs = deepCopyJsonable(args); try { SO(inboundHandler).inbound(...harden(safeArgs)); } catch (e) { diff --git a/packages/SwingSet/test/upgrade/upgrade-replay.test.js b/packages/SwingSet/test/upgrade/upgrade-replay.test.js index 1cd95fc32d2..1978de10bbf 100644 --- a/packages/SwingSet/test/upgrade/upgrade-replay.test.js +++ b/packages/SwingSet/test/upgrade/upgrade-replay.test.js @@ -4,6 +4,7 @@ import { test } from '../../tools/prepare-test-env-ava.js'; import { assert } from '@endo/errors'; import { kser } from '@agoric/kmarshal'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; import { initSwingStore } from '@agoric/swing-store'; import { buildKernelBundles, @@ -16,10 +17,6 @@ function bfile(name) { return new URL(name, import.meta.url).pathname; } -function copy(data) { - return JSON.parse(JSON.stringify(data)); -} - async function run(c, method, args = []) { assert(Array.isArray(args)); const kpid = c.queueToVatRoot('bootstrap', method, args); @@ -49,7 +46,12 @@ test('replay after upgrade', async t => { const ss1 = initSwingStore(); { - await initializeSwingset(copy(config), [], ss1.kernelStorage, initOpts); + await initializeSwingset( + deepCopyJsonable(config), + [], + ss1.kernelStorage, + initOpts, + ); const c1 = await makeSwingsetController(ss1.kernelStorage, {}, runtimeOpts); t.teardown(c1.shutdown); c1.pinVatRoot('bootstrap'); diff --git a/packages/SwingSet/test/vat-admin/replay.test.js b/packages/SwingSet/test/vat-admin/replay.test.js index 24cb2dfd24f..61658ad7fc3 100644 --- a/packages/SwingSet/test/vat-admin/replay.test.js +++ b/packages/SwingSet/test/vat-admin/replay.test.js @@ -2,14 +2,11 @@ // eslint-disable-next-line import/order import { test } from '../../tools/prepare-test-env-ava.js'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; import { kser } from '@agoric/kmarshal'; import { initSwingStore } from '@agoric/swing-store'; import { buildKernelBundles, buildVatController } from '../../src/index.js'; -function copy(data) { - return JSON.parse(JSON.stringify(data)); -} - test.before(async t => { const kernelBundles = await buildKernelBundles(); t.context.data = { kernelBundles }; @@ -34,7 +31,7 @@ test.serial('replay dynamic vat', async t => { const ss1 = initSwingStore(); { - const c1 = await buildVatController(copy(config), [], { + const c1 = await buildVatController(deepCopyJsonable(config), [], { kernelStorage: ss1.kernelStorage, kernelBundles: t.context.data.kernelBundles, }); @@ -52,7 +49,7 @@ test.serial('replay dynamic vat', async t => { const serialized = ss1.debug.serialize(); const ss2 = initSwingStore(null, { serialized }); { - const c2 = await buildVatController(copy(config), [], { + const c2 = await buildVatController(deepCopyJsonable(config), [], { kernelStorage: ss2.kernelStorage, }); t.teardown(c2.shutdown); diff --git a/packages/boot/test/upgrading/upgrade-vats.test.ts b/packages/boot/test/upgrading/upgrade-vats.test.ts index ed1647e1303..d5a3d22c14f 100644 --- a/packages/boot/test/upgrading/upgrade-vats.test.ts +++ b/packages/boot/test/upgrading/upgrade-vats.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @jessie.js/safe-await-separator -- test */ import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; -import { BridgeId } from '@agoric/internal'; +import { BridgeId, deepCopyJsonable } from '@agoric/internal'; import { buildVatController } from '@agoric/swingset-vat'; import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js'; import { Fail } from '@endo/errors'; @@ -434,8 +434,6 @@ test('upgrade vat-priceAuthority', async t => { matchRef(t, reincarnatedRegistry.adminFacet, registry.adminFacet); }); -const dataOnly = obj => JSON.parse(JSON.stringify(obj)); - test('upgrade vat-vow', async t => { const bundles = { vow: { @@ -483,7 +481,7 @@ test('upgrade vat-vow', async t => { }; await EV(vowRoot).makeLocalPromiseWatchers(localPromises); await EV(vowRoot).makeLocalVowWatchers(localVows); - t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { + t.deepEqual(deepCopyJsonable(await EV(vowRoot).getWatcherResults()), { promiseForever: { status: 'unsettled' }, promiseFulfilled: { status: 'fulfilled', value: 'hello' }, promiseRejected: { status: 'rejected', reason: 'goodbye' }, @@ -533,7 +531,7 @@ test('upgrade vat-vow', async t => { }); await EV(fakeVowKit.resolver).reject(upgradeRejection.reason); t.timeout(600_000); // t.timeout.clear() not yet available in our ava version - t.deepEqual(dataOnly(await EV(vowRoot).getWatcherResults()), { + t.deepEqual(deepCopyJsonable(await EV(vowRoot).getWatcherResults()), { promiseForever: upgradeRejection, promiseFulfilled: { status: 'fulfilled', value: 'hello' }, promiseRejected: { status: 'rejected', reason: 'goodbye' }, diff --git a/packages/cosmic-swingset/test/run-policy.test.js b/packages/cosmic-swingset/test/run-policy.test.js index 7a368621b76..e062b48f622 100644 --- a/packages/cosmic-swingset/test/run-policy.test.js +++ b/packages/cosmic-swingset/test/run-policy.test.js @@ -2,7 +2,7 @@ import test from 'ava'; import { assert, q, Fail } from '@endo/errors'; import { E } from '@endo/far'; -import { BridgeId, objectMap } from '@agoric/internal'; +import { BridgeId, deepCopyJsonable, objectMap } from '@agoric/internal'; import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; import { defaultBootstrapMessage, @@ -26,7 +26,7 @@ import { */ const makeSourceDescriptors = src => { const hardened = objectMap(src, sourceSpec => ({ sourceSpec })); - return JSON.parse(JSON.stringify(hardened)); + return deepCopyJsonable(hardened); }; /** diff --git a/packages/cosmic-swingset/tools/test-kit.js b/packages/cosmic-swingset/tools/test-kit.js index 88018ef591d..ee14da4a4e7 100644 --- a/packages/cosmic-swingset/tools/test-kit.js +++ b/packages/cosmic-swingset/tools/test-kit.js @@ -8,6 +8,7 @@ import { QueuedActionType, } from '@agoric/internal/src/action-types.js'; import { makeInitMsg } from '@agoric/internal/src/chain-utils.js'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; import { initSwingStore } from '@agoric/swing-store'; import { makeSlogSender } from '@agoric/telemetry'; import { launch } from '../src/launch-chain.js'; @@ -28,9 +29,6 @@ import { makeQueue, makeQueueStorageMock } from '../src/helpers/make-queue.js'; * @typedef {(input: T) => T} Replacer */ -/** @type {Replacer} */ -const deepCopyData = obj => JSON.parse(JSON.stringify(obj)); - /** @type {Replacer} */ const stripUndefined = obj => Object.fromEntries( @@ -62,7 +60,7 @@ export const defaultInitMessage = harden( }), ); export const defaultBootstrapMessage = harden({ - ...deepCopyData(defaultInitMessage), + ...deepCopyJsonable(defaultInitMessage), blockHeight: 1, blockTime: Math.floor(Date.parse('2010-01-01T00:00Z') / 1000), isBootstrap: true, @@ -184,7 +182,7 @@ export const makeCosmicSwingsetTestKit = async ( await null; /** @type {SwingSetConfig} */ let config = { - ...deepCopyData(baseConfig), + ...deepCopyJsonable(baseConfig), ...configOverrides, ...stripUndefined({ defaultManagerType }), }; @@ -209,7 +207,7 @@ export const makeCosmicSwingsetTestKit = async ( if (fixupConfig) config = fixupConfig(config); - let initMessage = deepCopyData(defaultInitMessage); + let initMessage = deepCopyJsonable(defaultInitMessage); if (fixupInitMessage) initMessage = fixupInitMessage(initMessage); initMessage?.type === SwingsetMessageType.AG_COSMOS_INIT || Fail`initMessage must be AG_COSMOS_INIT`; diff --git a/packages/internal/src/js-utils.js b/packages/internal/src/js-utils.js index 88bf990bc0d..70cd6d19c4b 100644 --- a/packages/internal/src/js-utils.js +++ b/packages/internal/src/js-utils.js @@ -5,6 +5,17 @@ * dependent upon a hardened environment. */ +/** + * Deep-copy a value by round-tripping it through JSON (which drops + * function/symbol/undefined values and properties that are non-enumerable + * and/or symbol-keyed, and rejects bigint values). + * + * @template T + * @param {T} value + * @returns {T} + */ +export const deepCopyJsonable = value => JSON.parse(JSON.stringify(value)); + /** * @param {any} value * @param {string | undefined} name diff --git a/packages/internal/test/snapshots/exports.test.js.md b/packages/internal/test/snapshots/exports.test.js.md index 036ef8a505c..9da2a6a2453 100644 --- a/packages/internal/test/snapshots/exports.test.js.md +++ b/packages/internal/test/snapshots/exports.test.js.md @@ -22,6 +22,7 @@ Generated by [AVA](https://avajs.dev). 'assertAllDefined', 'bindAllMethods', 'cast', + 'deepCopyJsonable', 'deepMapObject', 'deeplyFulfilledObject', 'forever', diff --git a/packages/internal/test/snapshots/exports.test.js.snap b/packages/internal/test/snapshots/exports.test.js.snap index b998abae9ce..730293f20f6 100644 Binary files a/packages/internal/test/snapshots/exports.test.js.snap and b/packages/internal/test/snapshots/exports.test.js.snap differ diff --git a/packages/solo/src/vat-http.js b/packages/solo/src/vat-http.js index 6f6b2b93653..1f79ca8a29f 100644 --- a/packages/solo/src/vat-http.js +++ b/packages/solo/src/vat-http.js @@ -3,6 +3,7 @@ import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; import { Far } from '@endo/marshal'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; import { makeNotifierKit } from '@agoric/notifier'; import { makeCache } from '@agoric/cache'; import { getReplHandler } from '@agoric/vats/src/repl.js'; @@ -82,7 +83,7 @@ export function buildRootObject(vatPowers) { D(commandDevice).sendResponse( count, isException, - obj || JSON.parse(JSON.stringify(obj)), + obj || deepCopyJsonable(obj), ); // Map an URL only to its latest handler. @@ -186,7 +187,7 @@ export function buildRootObject(vatPowers) { // Launder the data, since the command device tends to pass device nodes // when there are empty objects, which screw things up for us. // Analysis is in https://github.com/Agoric/agoric-sdk/pull/1956 - const obj = JSON.parse(JSON.stringify(rawObj)); + const obj = deepCopyJsonable(rawObj); console.debug( `vat-http.inbound (from browser) ${count}`, JSON.stringify(obj, undefined, 2), diff --git a/packages/wallet/api/src/wallet.js b/packages/wallet/api/src/wallet.js index b00a28a9245..7efc95ede75 100644 --- a/packages/wallet/api/src/wallet.js +++ b/packages/wallet/api/src/wallet.js @@ -9,8 +9,9 @@ * types.js file. */ import { E } from '@endo/eventual-send'; -import { makeNotifierKit, observeIteration } from '@agoric/notifier'; import { Far } from '@endo/marshal'; +import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js'; +import { makeNotifierKit, observeIteration } from '@agoric/notifier'; import { makeWalletRoot } from './lib-wallet.js'; import pubsub from './pubsub.js'; @@ -54,7 +55,7 @@ export function buildRootObject(vatPowers) { const offerSubscriptions = new Map(); const httpSend = (obj, channelHandles) => - E(http).send(JSON.parse(JSON.stringify(obj)), channelHandles); + E(http).send(deepCopyJsonable(obj), channelHandles); const pushOfferSubscriptions = (channelHandle, offers) => { const subs = offerSubscriptions.get(channelHandle); diff --git a/packages/wallet/package.json b/packages/wallet/package.json index 42dc3336f9e..c25d2b53b0b 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -19,6 +19,7 @@ "build": "exit 0" }, "dependencies": { + "@agoric/internal": "^0.3.2", "@agoric/wallet-ui": "0.1.3-solo.0", "babel-eslint": "^10.0.3", "eslint-plugin-eslint-comments": "^3.1.2",