Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vats): upgradeable board #6903

Merged
merged 13 commits into from
Feb 8, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ test('ertp service upgrade', async t => {
// defaultReapInterval: 'never',
// defaultReapInterval: 1,
vats: {
// TODO refactor to use bootstrap-relay.js
bootstrap: { sourceSpec: bfile('bootstrap-ertp-service-upgrade.js') },
},
bundles: {
Expand Down
37 changes: 34 additions & 3 deletions packages/SwingSet/test/bootstrap-relay.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,42 @@
import { E } from '@endo/eventual-send';
import { Far, isObject, makeMarshal } from '@endo/marshal';
import { assert } from '@agoric/assert';
import { objectMap } from '@agoric/internal';
import { buildManualTimer } from '../tools/manual-timer.js';

const { Fail, quote: q } = assert;

const sink = () => {};

// TODO define these somewhere more accessible. https://github.com/endojs/endo/issues/1488
/**
* @typedef {Promise | import('@agoric/internal').Remotable} PassByRef
* Gets transformed by a marshaller encoding.
* As opposed to pass-by-copy
*/

export const buildRootObject = () => {
const timer = buildManualTimer();
let vatAdmin;
const vatData = new Map();

// Represent all data as passable by replacing non-passable values
// with special-prefix registered symbols.
/** @type {Map<symbol, PassByRef>} */
const replaced = new Map();
/** @type {Map<PassByRef, symbol>} */
const replacements = new Map(); // inverse of 'replaced'

// This is testing code, so we don't enforce absence of this prefix
// from manually created symbols.
const replacementPrefix = 'replaced:';
const makeReplacement = value => {
const provideReplacement = value => {
if (replacements.has(value)) {
return replacements.get(value);
}

const replacement = Symbol.for(`${replacementPrefix}${replaced.size}`);
replacements.set(value, replacement);
replaced.set(replacement, value);

// Suppress unhandled promise rejection warnings.
Expand All @@ -28,7 +45,7 @@ export const buildRootObject = () => {
return replacement;
};
const { serialize: encodeReplacements } = makeMarshal(
makeReplacement,
provideReplacement,
undefined,
{
marshalSaveError: () => {},
Expand Down Expand Up @@ -57,7 +74,7 @@ export const buildRootObject = () => {
} else if (
typeof arg !== 'symbol' ||
!Symbol.keyFor(arg) ||
!arg.description.startsWith(replacementPrefix)
!arg.description?.startsWith(replacementPrefix)
) {
return arg;
}
Expand Down Expand Up @@ -97,6 +114,20 @@ export const buildRootObject = () => {
return incarnationNumber;
},

/**
* Turns an object into a remotable by ensuring that each property is a function
*
* @param {string} label
* @param {Record<string, any>} methodReturnValues
*/
makeSimpleRemotable: (label, methodReturnValues) =>
// braces to unharden so it can be hardened
encodePassable(
Far(label, {
...objectMap(methodReturnValues, v => () => decodePassable(v)),
}),
),

messageVat: async ({ name, methodName, args = [] }) => {
const vat = vatData.get(name) || Fail`unknown vat name: ${q(name)}`;
const { root } = vat;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ test('vat-timer upgrade', async t => {
const config = {
bootstrap: 'bootstrap',
vats: {
// TODO refactor to use bootstrap-relay.js
bootstrap: { sourceSpec: bfile('bootstrap-vat-timer-upgrade.js') },
},
devices: { timer: { sourceSpec: timer.srcPath } },
Expand Down
9 changes: 7 additions & 2 deletions packages/inter-protocol/test/test-gov-collateral.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Stable } from '@agoric/vats/src/tokens.js';
import { makeNodeBundleCache } from '@agoric/swingset-vat/tools/bundleTool.js';
import { TimeMath } from '@agoric/time';

import { makeScalarBigMapStore } from '@agoric/vat-data';
import {
setupBootstrap,
setUpZoeForTest,
Expand Down Expand Up @@ -124,8 +125,12 @@ test.before(async t => {
const makeScenario = async (t, { env = process.env } = {}) => {
const space = await setupBootstrap(t);

const loadVat = name =>
import(`@agoric/vats/src/vat-${name}.js`).then(ns => ns.buildRootObject());
const loadVat = name => {
const baggage = makeScalarBigMapStore('baggage');
return import(`@agoric/vats/src/vat-${name}.js`).then(ns =>
ns.buildRootObject({}, {}, baggage),
);
};
space.produce.loadVat.resolve(loadVat);
space.produce.loadCriticalVat.resolve(loadVat);

Expand Down
18 changes: 8 additions & 10 deletions packages/smart-wallet/src/marshal-contexts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@
import { makeScalarMapStore } from '@agoric/store';
import { Far, makeMarshal, Remotable } from '@endo/marshal';
import { HandledPromise } from '@endo/eventual-send'; // TODO: convince tsc this isn't needed
import { DEFAULT_PREFIX } from '@agoric/vats/src/lib-board.js';

const { Fail, quote: q } = assert;

/**
* For a value with a known id in the board, we can use
* that board id as a slot to preserve identity when marshaling.
*
* @typedef {`board${Digits}`} BoardId
*/
/** @typedef {import('@agoric/vats/src/lib-board.js').BoardId} BoardId */

/**
* ID from a board made with { prefix: DEFAULT_PREFIX }
*
* @param {unknown} specimen
* @returns {specimen is BoardId}
*/
const isBoardId = specimen => {
return typeof specimen === 'string' && !!specimen.match(/^board[^:]/);
const isDefaultBoardId = specimen => {
return typeof specimen === 'string' && specimen.startsWith(DEFAULT_PREFIX);
};

/**
Expand Down Expand Up @@ -151,7 +149,7 @@ export const makeExportContext = () => {
* @param {string} _iface
*/
const slotToVal = (slot, _iface) => {
if (isBoardId(slot) && boardObjects.bySlot.has(slot)) {
if (isDefaultBoardId(slot) && boardObjects.bySlot.has(slot)) {
return boardObjects.bySlot.get(slot);
}
const { kind, id } = parseWalletSlot(walletObjects, slot);
Expand Down Expand Up @@ -288,7 +286,7 @@ export const makeImportContext = (makePresence = defaultMakePresence) => {
* @param {string} iface
*/
fromBoard: (slot, iface) => {
isBoardId(slot) || Fail`bad board slot ${q(slot)}`;
isDefaultBoardId(slot) || Fail`bad board slot ${q(slot)}`;
return provideVal(boardObjects, slot, iface);
},

Expand Down
7 changes: 5 additions & 2 deletions packages/smart-wallet/test/supports.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js';
import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.js';
import { makeLoopback } from '@endo/captp';
import { E, Far } from '@endo/far';
import { makeScalarBigMapStore } from '@agoric/vat-data';

export { ActionType };

Expand Down Expand Up @@ -128,8 +129,10 @@ export const makeMockTestSpace = async log => {
switch (name) {
case 'mints':
return mintsRoot();
case 'board':
return boardRoot();
case 'board': {
const baggage = makeScalarBigMapStore('baggage');
return boardRoot({}, {}, baggage);
}
default:
throw Error('unknown loadVat name');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { makeHandle } from '@agoric/zoe/src/makeHandle.js';
import {
makeExportContext,
makeImportContext,
} from '@agoric/smart-wallet/src/marshal-contexts.js';
} from '../src/marshal-contexts.js';

/** @param {ReturnType<typeof makeBoard>} board */
const makeAMM = board => {
Expand Down Expand Up @@ -111,21 +111,21 @@ test('ensureBoardId allows re-registration; initBoardId does not', t => {
const brandS = Far('Semolean brand', {});

const context = makeExportContext();
context.initBoardId('board1', brandM);
t.throws(() => context.initBoardId('board1', brandM));
context.ensureBoardId('board1', brandM);
t.throws(() => context.ensureBoardId('board12', brandM));
context.ensureBoardId('board12', brandS);
t.throws(() => context.initBoardId('board12', brandM));
context.initBoardId('board01', brandM);
t.throws(() => context.initBoardId('board01', brandM));
context.ensureBoardId('board01', brandM);
t.throws(() => context.ensureBoardId('board012', brandM));
context.ensureBoardId('board012', brandS);
t.throws(() => context.initBoardId('board012', brandM));
});

test('makeExportContext.serialize handles unregistered identites', t => {
test('makeExportContext.serialize handles unregistered identities', t => {
const brand = Far('Zoe invitation brand', {});
const instance = Far('amm instance', {});
const invitationAmount = harden({ brand, value: [{ instance }] });

const context = makeExportContext();
context.initBoardId('board1', brand);
context.initBoardId('board01', brand);
const actual = context.serialize(invitationAmount);

t.deepEqual(actual, {
Expand All @@ -145,7 +145,7 @@ test('makeExportContext.serialize handles unregistered identites', t => {
},
],
}),
slots: ['board1', 'unknown:1'],
slots: ['board01', 'unknown:1'],
});

t.deepEqual(context.unserialize(actual), invitationAmount);
Expand Down Expand Up @@ -176,9 +176,9 @@ test('fromBoard.serialize requires board ids', t => {
message: '"key" not found: "[Alleged: InstanceHandle]"',
});

context.ensureBoardId('board123', unpassable.instance);
context.ensureBoardId('board0123', unpassable.instance);
t.deepEqual(context.fromBoard.serialize(unpassable), {
body: '{"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":0}}',
slots: ['board123'],
slots: ['board0123'],
});
});
1 change: 1 addition & 0 deletions packages/vats/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@agoric/store": "^0.8.3",
"@agoric/swingset-vat": "^0.30.2",
"@agoric/time": "^0.2.1",
"@agoric/vat-data": "^0.4.3",
"@agoric/zoe": "^0.25.3",
"@endo/far": "^0.2.14",
"@endo/import-bundle": "^0.3.0",
Expand Down
Loading