Skip to content

Commit

Permalink
feat(swingset): allow object refs in create/upgrade/terminate vat
Browse files Browse the repository at this point in the history
This allows `E(vatAdminService).createVat(bundleID, { vatParameters })` to
include object references in `vatParameters`, which are then delivered
through the `dispatch.startVat()` delivery and made available to
`buildRootObject(vatPowers, vatParameters)`.

From within a vat, `vatPowers.exitVat(completion)` and
`.exitVatWithFailure(reason)` can include object references, and they will be
delivered to the parent caller's `done` promise.

From outside the vat, `E(adminNode).terminateWithFailure(reason)` can take
object refs in `reason` and they will be delivered to the parent caller's
`done` promise. `E(adminNode).upgrade(bundleID, vatParameters)` can take
object refs in `vatParameters` just like `createVat` (although `upgrade`
itself is still non-functional).

The kernel will maintain a refcount on each object passed through this
mechanism, to keep it from being collected while in transit.

This is implemented with the new "kernel device hooks" feature, which allows
a device to call into the kernel and have its drefs translated into krefs.

This will help with ZCF/contract vat upgrade, to pass the new contract
bundlecap into the new ZCF vat via vatParameters.

closes #4588
closes #4381
refs #1848
  • Loading branch information
warner committed Mar 19, 2022
1 parent 1abf5be commit 2bfcbc6
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 116 deletions.
59 changes: 17 additions & 42 deletions packages/SwingSet/src/devices/vat-admin/device-vat-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,6 @@ bundleID before submitting to the kernel), or (temporarily) a full bundle.

export function buildDevice(tools, endowments) {
const { hasBundle, getBundle, getNamedBundleID } = endowments;
const { pushCreateVatBundleEvent, pushCreateVatIDEvent } = endowments;
const { pushUpgradeVatEvent } = endowments;
const { terminate } = endowments;
const { meterCreate, meterAddRemaining, meterSetThreshold, meterGet } =
endowments;

Expand Down Expand Up @@ -194,65 +191,43 @@ export function buildDevice(tools, endowments) {

// D(devices.vatAdmin).createByBundle(bundle, options) -> vatID
if (method === 'createByBundle') {
// see #4588
assert.equal(argsCapdata.slots.length, 0, 'cannot handle refs');
const args = unserialize(argsCapdata);
const [bundle, options] = args;
// options cannot hold caps yet, but I expect #4486 will remove
// this case before #4381 needs them here
const vatID = pushCreateVatBundleEvent(bundle, options);
const res = syscall.callKernelHook('createByBundle', argsCapdata);
const vatID = JSON.parse(res.body);
assert.typeof(vatID, 'string', `createByBundle gave non-VatID`);
return returnFromInvoke(vatID);
}

// D(devices.vatAdmin).createByBundleID(bundleID, options) -> vatID
if (method === 'createByBundleID') {
// see #4588
assert.equal(argsCapdata.slots.length, 0, 'cannot handle refs');
const args = unserialize(argsCapdata);
const [bundleID, options] = args;
assert.typeof(bundleID, 'string', `createByBundleID() bundleID`);
// TODO: options cannot hold caps yet, #4381 will want them in
// vatParameters, follow the pattern in terminateWithFailure
const vatID = pushCreateVatIDEvent(bundleID, options);
const res = syscall.callKernelHook('createByID', argsCapdata);
const vatID = JSON.parse(res.body);
assert.typeof(vatID, 'string', `createByID gave non-VatID`);
return returnFromInvoke(vatID);
}

// D(devices.vatAdmin).upgradeVat(bundleID, vatParameters) > upgradeID
// D(devices.vatAdmin).upgradeVat(bundleID, vatParameters) -> upgradeID
if (method === 'upgradeVat') {
// see #4381, we really need to accept slots in vatParameters
// so we can pass contract bundlecaps to upgraded ZCF
const { body, slots } = argsCapdata;
// this has all the same problems described in terminateWithFailure
assert.equal(slots.length, 0, 'upgradeVat() cannot handle refs yet');
const args = JSON.parse(body);
const args = JSON.parse(argsCapdata.body);
assert(Array.isArray(args), 'upgradeVat() args array');
assert.equal(args.length, 2, `upgradeVat() args length`);
const [bundleID, vatParameters] = args;
const [bundleID, _vatParameters] = args;
assert.typeof(bundleID, 'string', `upgradeVat() bundleID`);
const vpCapdata = { body: JSON.stringify(vatParameters), slots };
const upgradeID = pushUpgradeVatEvent(bundleID, vpCapdata);

const res = syscall.callKernelHook('upgrade', argsCapdata);
const upgradeID = JSON.parse(res.body);
assert.typeof(upgradeID, 'string', 'upgradeVat gave non-upgradeID');
return returnFromInvoke(upgradeID);
}

// D(devices.vatAdmin).terminateWithFailure(vatID, reason)
if (method === 'terminateWithFailure') {
// 'args' is capdata([vatID, reason]), and comes from vatAdmin (but
// 'reason' comes from user code). We want to extract vatID and get
// capdata(reason) to send into terminate(). Don't use the
// deviceTools serialize(), it isn't a complete marshal and we just
// want to passthrough anyways.
const { body, slots } = argsCapdata;
// TODO: these slots are drefs, and the kernel terminate()
// endowment needs krefs, so we forbid them entirely for now
// see #4588
assert.equal(slots.length, 0, 'cannot handle refs in terminate');
const args = JSON.parse(body);
const args = JSON.parse(argsCapdata.body);
assert(Array.isArray(args), 'terminateWithFailure() args array');
assert.equal(args.length, 2, `terminateWithFailure() args length`);
const [vatID, reason] = args;
const [vatID, _reason] = args;
assert.typeof(vatID, 'string', `terminateWithFailure() vatID`);
const reasonCapdata = { body: JSON.stringify(reason), slots };
terminate(vatID, reasonCapdata);

syscall.callKernelHook('terminate', argsCapdata);
return returnFromInvoke(undefined);
}

Expand Down
65 changes: 13 additions & 52 deletions packages/SwingSet/src/kernel/kernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { processNextGCAction } from './gc-actions.js';
import { makeVatLoader } from './vat-loader/vat-loader.js';
import { makeDeviceTranslators } from './deviceTranslator.js';
import { notifyTermination } from './notifyTermination.js';
import { makeVatAdminHooks } from './vat-admin-hooks.js';

function abbreviateReplacer(_, arg) {
if (typeof arg === 'bigint') {
Expand Down Expand Up @@ -548,6 +549,11 @@ export default function buildKernel(
const kd = harden(['startVat', vatParameters]);
// eslint-disable-next-line no-use-before-define
const vd = vatWarehouse.kernelDeliveryToVatDelivery(vatID, kd);
// decref slots now that create-vat is off run-queue
for (const kref of vatParameters.slots) {
kernelKeeper.decrementRefCount(kref, 'create-vat-event');
}

// TODO: can we provide a computron count to the run policy?
const status = await deliverAndLogToVat(vatID, kd, vd);
return { ...status, discardFailedDelivery: true };
Expand Down Expand Up @@ -1266,58 +1272,6 @@ export default function buildKernel(
hasBundle: kernelKeeper.hasBundle,
getBundle: kernelKeeper.getBundle,
getNamedBundleID: kernelKeeper.getNamedBundleID,
pushCreateVatBundleEvent(bundle, dynamicOptions) {
// TODO: translate dynamicOptions.vatParameters.slots from dref to kref
const source = { bundle };
const { vatParameters: rawVP, ...rest } = dynamicOptions;
const vatParameters = { body: stringify(harden(rawVP)), slots: [] };
insistCapData(vatParameters);
dynamicOptions = rest;
const vatID = kernelKeeper.allocateUnusedVatID();
const event = {
type: 'create-vat',
vatID,
source,
vatParameters,
dynamicOptions,
};
kernelKeeper.addToAcceptanceQueue(harden(event));
// the device gets the new vatID immediately, and will be notified
// later when it is created and a root object is available
return vatID;
},
pushCreateVatIDEvent(bundleID, dynamicOptions) {
assert(kernelKeeper.hasBundle(bundleID), bundleID);
// TODO: translate dynamicOptions.vatParameters.slots from dref to kref
const source = { bundleID };
const { vatParameters: rawVP, ...rest } = dynamicOptions;
const vatParameters = { body: stringify(harden(rawVP)), slots: [] };
insistCapData(vatParameters);
dynamicOptions = rest;
const vatID = kernelKeeper.allocateUnusedVatID();
const event = {
type: 'create-vat',
vatID,
source,
vatParameters,
dynamicOptions,
};
kernelKeeper.addToAcceptanceQueue(harden(event));
// the device gets the new vatID immediately, and will be notified
// later when it is created and a root object is available
return vatID;
},
pushUpgradeVatEvent(bundleID, rawVP) {
const vatParameters = { body: stringify(harden(rawVP)), slots: [] };
insistCapData(vatParameters);
const upgradeID = kernelKeeper.allocateUpgradeID();
// TODO: translate vatParameters.slots from dref to kref
// TODO: incref both bundleID and slots in vatParameters
const ev = { type: 'upgrade-vat', upgradeID, bundleID, vatParameters };
kernelKeeper.addToAcceptanceQueue(harden(ev));
return upgradeID;
},
terminate: (vatID, reason) => terminateVat(vatID, true, reason),
meterCreate: (remaining, threshold) =>
kernelKeeper.allocateMeter(remaining, threshold),
meterAddRemaining: (meterID, delta) =>
Expand Down Expand Up @@ -1386,6 +1340,13 @@ export default function buildKernel(
}
}

// attach vat-admin device hooks
const vatAdminDeviceID = kernelKeeper.getDeviceIDForName('vatAdmin');
if (vatAdminDeviceID) {
const hooks = makeVatAdminHooks({ kernelKeeper, terminateVat });
deviceHooks.set(vatAdminDeviceID, hooks);
}

kernelKeeper.loadStats();
}

Expand Down
114 changes: 114 additions & 0 deletions packages/SwingSet/src/kernel/vat-admin-hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { assert } from '@agoric/assert';
import { stringify, parse } from '@endo/marshal';
import { insistVatID } from '../lib/id.js';

export function makeVatAdminHooks(tools) {
const { kernelKeeper, terminateVat } = tools;
return {
createByBundle(argsCapData) {
// first, split off vatParameters
const argsJSON = JSON.parse(argsCapData.body);
const [bundle, { vatParameters: vpJSON, ...dynamicOptionsJSON }] =
argsJSON;
// assemble the vatParameters capdata
const vatParameters = {
body: JSON.stringify(vpJSON),
slots: argsCapData.slots,
};
// then re-parse the rest with marshal
const dynamicOptions = parse(JSON.stringify(dynamicOptionsJSON));
// incref slots while create-vat is on run-queue
for (const kref of vatParameters.slots) {
kernelKeeper.incrementRefCount(kref, 'create-vat-event');
}
const source = { bundle };
const vatID = kernelKeeper.allocateUnusedVatID();
const event = {
type: 'create-vat',
vatID,
source,
vatParameters,
dynamicOptions,
};
kernelKeeper.addToAcceptanceQueue(harden(event));
// the device gets the new vatID immediately, and will be notified
// later when it is created and a root object is available
const vatIDCapData = { body: JSON.stringify(vatID), slots: [] };
return harden(vatIDCapData);
},

createByID(argsCapData) {
// argsCapData is marshal([bundleID, options]), and options is {
// vatParameters, ...rest }, and 'rest' is JSON-serializable (no
// slots or bigints or undefined). We get the intermediate marshal
// representation (with @qclass nodes), carve off vatParameters,
// then reassemble the rest. All slots will be associated with
// vatParameters.

// first, split off vatParameters
const argsJSON = JSON.parse(argsCapData.body);
const [bundleID, { vatParameters: vpJSON, ...dynamicOptionsJSON }] =
argsJSON;
assert(kernelKeeper.hasBundle(bundleID), bundleID);
// assemble the vatParameters capdata
const vatParameters = {
body: JSON.stringify(vpJSON),
slots: argsCapData.slots,
};
// then re-parse the rest with marshal
const dynamicOptions = parse(JSON.stringify(dynamicOptionsJSON));
// incref slots while create-vat is on run-queue
for (const kref of vatParameters.slots) {
kernelKeeper.incrementRefCount(kref, 'create-vat-event');
}
const source = { bundleID };
const vatID = kernelKeeper.allocateUnusedVatID();
const event = {
type: 'create-vat',
vatID,
source,
vatParameters,
dynamicOptions,
};
kernelKeeper.addToAcceptanceQueue(harden(event));
// the device gets the new vatID immediately, and will be notified
// later when it is created and a root object is available
const vatIDCapData = { body: JSON.stringify(vatID), slots: [] };
return harden(vatIDCapData);
},

upgrade(argsCapData) {
// marshal([bundleID, vatParameters]) -> upgradeID
const argsJSON = JSON.parse(argsCapData.body);
const [bundleID, vpJSON] = argsJSON;
assert.typeof(bundleID, 'string');
const vpCD = { body: JSON.stringify(vpJSON), slots: argsCapData.slots };
for (const kref of vpCD.slots) {
kernelKeeper.incrementRefCount(kref, 'upgrade-vat-event');
}
const upgradeID = kernelKeeper.allocateUpgradeID();
const ev = {
type: 'upgrade-vat',
upgradeID,
bundleID,
vatParameters: vpCD,
};
kernelKeeper.addToAcceptanceQueue(harden(ev));
const upgradeIDCD = { body: JSON.stringify(upgradeID), slots: [] };
return harden(upgradeIDCD);
},

terminate(argsCD) {
// marshal([vatID, reason]) -> null
const argsJSON = JSON.parse(argsCD.body);
const [vatID, reasonJSON] = argsJSON;
insistVatID(vatID);
const reasonCD = { ...argsCD, body: JSON.stringify(reasonJSON) };
// we don't need to incrementRefCount because if terminateVat sends
// 'reason' to vat-admin, it uses notifyTermination / queueToKref /
// doSend, and doSend() does its own incref
terminateVat(vatID, true, reasonCD);
return harden({ body: stringify(undefined), slots: [] });
},
};
}
12 changes: 4 additions & 8 deletions packages/SwingSet/src/kernel/vatTranslator.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,11 @@ function makeTranslateKernelDeliveryToVatDelivery(vatID, kernelKeeper) {
* @returns { VatDeliveryStartVat }
*/
function translateStartVat(kernelVP) {
const slots = kernelVP.slots.map(slot => mapKernelSlotToVatSlot(slot));
const vatVP = { ...kernelVP, slots };
/** @type { VatDeliveryStartVat } */
return harden(['startVat', kernelVP]); // TODO until capdata
// const vatVP = {
// ...kernelVP,
// slots: kernelVP.slots.map(slot => mapKernelSlotToVatSlot(slot)),
// };
// /** @type { VatDeliveryStartVat } */
// const startVatMessageVatDelivery = harden(['startVat', vatVP]);
// return startVatMessageVatDelivery;
const startVatMessageVatDelivery = harden(['startVat', vatVP]);
return startVatMessageVatDelivery;
}

function translateBringOutYourDead() {
Expand Down
28 changes: 23 additions & 5 deletions packages/SwingSet/test/vat-admin/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,51 @@ import { Far } from '@endo/marshal';

export function buildRootObject() {
let admin;
let held;

const adder = Far('adder', { add1: x => x + 1 });
const options = { vatParameters: { adder } };

return Far('root', {
async bootstrap(vats, devices) {
admin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin);
held = await E(vats['export-held']).createHeld();
},

async byBundle(bundle) {
const { root } = await E(admin).createVat(bundle);
const { root } = await E(admin).createVat(bundle, options);
const n = await E(root).getANumber();
return n;
},

async byName(bundleName) {
const { root } = await E(admin).createVatByName(bundleName);
const { root } = await E(admin).createVatByName(bundleName, options);
const n = await E(root).getANumber();
return n;
},

async byNamedBundleCap(name) {
const bcap = await E(admin).getNamedBundleCap(name);
const { root } = await E(admin).createVat(bcap);
const { root } = await E(admin).createVat(bcap, options);
const n = await E(root).getANumber();
return n;
},

async byID(id) {
const bcap = await E(admin).getBundleCap(id);
const { root } = await E(admin).createVat(bcap);
const { root } = await E(admin).createVat(bcap, options);
const n = await E(root).getANumber();
return n;
},

async counters(bundleName) {
const { root } = await E(admin).createVatByName(bundleName);
const { root } = await E(admin).createVatByName(bundleName, options);
const c = E(root).createRcvr(1);
const log = [];
log.push(await E(c).increment(3));
log.push(await E(c).increment(5));
log.push(await E(c).ticker());
log.push(await E(c).add2(6));
return log;
},

Expand All @@ -52,5 +58,17 @@ export function buildRootObject() {
async nonBundleCap() {
return E(admin).createVat(Far('non-bundlecap', {})); // should reject
},

getHeld() {
return held;
},

refcount(id) {
// bootstrap retains 'held' the whole time, contributing one refcount
return E(admin)
.getBundleCap(id)
.then(bcap => E(admin).createVat(bcap, { vatParameters: { held } }))
.then(() => 0);
},
});
}
Loading

0 comments on commit 2bfcbc6

Please sign in to comment.