Skip to content

Commit

Permalink
record durable-kind stateShape, with refcounts
Browse files Browse the repository at this point in the history
Save a serialized copy of the Kind's `stateShape` option, so future
incarnations can compare the old one against newer ones when they
re-define the Kind. Increment refcounts on any objects included in the
shape. Forbid the use of non-durable objects in the shape.

closes #7338
refs #7337
  • Loading branch information
warner committed Apr 9, 2023
1 parent 3c66eba commit 2ee1745
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 22 deletions.
67 changes: 47 additions & 20 deletions packages/swingset-liveslots/src/virtualObjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,18 @@ const makeContextProviderKit = (contextCache, getSlotForVal, facetNames) => {
return harden(contextProviderKit);
};

function checkAndUpdateFacetiousness(
tag,
desc,
facetNames,
saveDurableKindDescriptor,
) {
function checkAndUpdateFacetiousness(tag, desc, facetNames) {
// The first time a durable kind gets a definition, the saved descriptor
// will have neither ".unfaceted" nor ".facets", and we must record the
// initial details in the descriptor.
// will have neither ".unfaceted" nor ".facets", and we must update the
// details in the descriptor.

if (!desc.unfaceted && !desc.facets) {
if (facetNames) {
desc.facets = facetNames;
} else {
desc.unfaceted = true;
}
saveDurableKindDescriptor(desc);
return;
return; // caller will saveDurableKindDescriptor()
}

// When a later incarnation redefines the behavior, it must match.
Expand Down Expand Up @@ -283,8 +277,8 @@ function insistDurableCapdata(vrm, what, capdata, valueFor) {
* @param {(slot: string) => object} requiredValForSlot
* @param {*} registerValue Function to register a new slot+value in liveSlot's
* various tables
* @param {import('@endo/marshal').Serialize<unknown>} serialize Serializer for this vat
* @param {import('@endo/marshal').Unserialize<unknown>} unserialize Unserializer for this vat
* @param {import('@endo/marshal').Serialize<string>} serialize Serializer for this vat
* @param {import('@endo/marshal').Unserialize<string>} unserialize Unserializer for this vat
* @param {*} assertAcceptableSyscallCapdataSize Function to check for oversized
* syscall params
*
Expand Down Expand Up @@ -533,6 +527,7 @@ export function makeVirtualObjectManager(
* tag: string,
* unfaceted?: boolean,
* facets?: string[],
* stateShapeCapData?: import('./types.js').SwingSetCapData
* }} DurableKindDescriptor
*/

Expand Down Expand Up @@ -680,21 +675,53 @@ export function makeVirtualObjectManager(
}
// beyond this point, we use 'multifaceted' to switch modes

if (isDurable) {
checkAndUpdateFacetiousness(
tag,
durableKindDescriptor,
facetNames,
saveDurableKindDescriptor,
);
}
// The 'stateShape' pattern constrains the `state` of each
// instance: which properties it may have, and what their values
// are allowed to be. For durable Kinds, the stateShape is
// serialized and recorded in the durableKindDescriptor, so future
// incarnations (which redefine the kind when they call
// defineDurableKind again)

harden(stateShape);
stateShape === undefined ||
passStyleOf(stateShape) === 'copyRecord' ||
Fail`A stateShape must be a copyRecord: ${q(stateShape)}`;
assertPattern(stateShape);

if (isDurable) {
// durableKindDescriptor is created by makeKindHandle, with just
// { kindID, tag, nextInstanceID }, then the first
// defineDurableKind (maybe us!) will populate
// .facets/.unfaceted and a .stateShape . We'll only see those
// properties if we're in a non-initial incarnation.

assert(durableKindDescriptor);

// initial creation will update the descriptor with .facets or
// .unfaceted, subsequent re-definitions will just assert
// compatibility
checkAndUpdateFacetiousness(tag, durableKindDescriptor, facetNames);

const stateShapeCapData = serialize(stateShape);

// Durable kinds can only hold durable objects in their state,
// so if the stateShape were to require a non-durable object,
// nothing could ever match. So we require the shape have only
// durable objects
insistDurableCapdata(vrm, 'stateShape', stateShapeCapData, false);

// compare against slots of previous definition, incref/decref
let oldStateShapeSlots = [];
if (durableKindDescriptor.stateShapeCapData) {
oldStateShapeSlots = durableKindDescriptor.stateShapeCapData.slots;
}
const newStateShapeSlots = stateShapeCapData.slots;
vrm.updateReferenceCounts(oldStateShapeSlots, newStateShapeSlots);
durableKindDescriptor.stateShapeCapData = stateShapeCapData; // replace

saveDurableKindDescriptor(durableKindDescriptor);
}

let checkStateProperty = _prop => undefined;
/** @type {(value: any, prop: string) => void} */
let checkStatePropertyValue = (_value, _prop) => undefined;
Expand Down
199 changes: 199 additions & 0 deletions packages/swingset-liveslots/test/virtual-objects/test-state-shape.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import test from 'ava';
import '@endo/init/debug.js';

import { Far } from '@endo/marshal';
import { M } from '@agoric/store';
import { makeLiveSlots } from '../../src/liveslots.js';
import { kser, kslot } from '../kmarshal.js';
import { buildSyscall } from '../liveslots-helpers.js';
import { makeStartVat, makeMessage } from '../util.js';
import { makeMockGC } from '../mock-gc.js';
import { makeFakeVirtualStuff } from '../../tools/fakeVirtualSupport.js';

function makeGenericRemotable(typeName) {
return Far(typeName, {
aMethod() {
return 'whatever';
},
});
}
const eph1 = makeGenericRemotable('ephemeral1');
const eph2 = makeGenericRemotable('ephemeral2');

const init = value => ({ value });
const behavior = {
set: ({ state }, value) => (state.value = value),
};

// virtual/durable Kinds can specify a 'stateShape', which should be
// enforced, both during initialization and subsequent state changes

test('constrain state shape', t => {
const { vom } = makeFakeVirtualStuff();
const { defineKind } = vom;
const any = { value: M.any() };
const number = { value: M.number() };
const string = { value: M.string() };
const remotable = { value: M.remotable() };
const eph = { value: eph1 };

// M.any() allows anything
const makeA = defineKind('kindA', init, behavior, { stateShape: any });
makeA(eph1);
makeA(1);
makeA('string');
const a = makeA(1);
a.set(eph1);
a.set(2);
a.set('other string');

// M.number() requires a number
const numberFail = { message: /Must be a number/ };
const makeB = defineKind('kindB', init, behavior, { stateShape: number });
t.throws(() => makeB(eph1), numberFail);
const b = makeB(1);
t.throws(() => makeB('string'), numberFail);
t.throws(() => b.set(eph1), numberFail);
t.throws(() => b.set('string'), numberFail);

// M.string() requires a string
const stringFail = { message: /Must be a string/ };
const makeC = defineKind('kindC', init, behavior, { stateShape: string });
t.throws(() => makeC(eph1), stringFail);
const c = makeC('string');
t.throws(() => makeC(1), stringFail);
t.throws(() => c.set(eph1), stringFail);
t.throws(() => c.set(2), stringFail);

// M.remotable() requires any Remotable
const remotableFail = { message: /Must be a remotable/ };
const makeD = defineKind('kindD', init, behavior, { stateShape: remotable });
const d = makeD(eph1);
makeD(eph2);
t.throws(() => makeD(1), remotableFail);
t.throws(() => makeD('string'), remotableFail);
d.set(eph2);
t.throws(() => d.set(2), remotableFail);
t.throws(() => d.set('string'), remotableFail);

// using a specific Remotable object requires that exact object
const eph1Fail = { message: /Must be:.*Alleged: ephemeral1/ };
const makeE = defineKind('kindE', init, behavior, { stateShape: eph });
const e = makeE(eph1);
t.throws(() => makeE(eph2), eph1Fail);
t.throws(() => makeE(1), eph1Fail);
t.throws(() => makeE('string'), eph1Fail);
e.set(eph1);
t.throws(() => e.set(eph2), eph1Fail);
t.throws(() => e.set(2), eph1Fail);
t.throws(() => e.set('string'), eph1Fail);
});

// durable Kinds serialize and store their stateShape, which must
// itself be durable

test('durable state shape', t => {
// note: relaxDurabilityRules defaults to true in fake tools
const { vom } = makeFakeVirtualStuff({ relaxDurabilityRules: false });
const { makeKindHandle, defineDurableKind } = vom;

const make = (which, stateShape) => {
const kh = makeKindHandle(`kind${which}`);
return defineDurableKind(kh, init, behavior, { stateShape });
};

const makeKind1 = make(1);
makeKind1();

const makeKind2 = make(2);
makeKind2();

const makeKind3 = make(3, { value: M.any() });
const obj3 = makeKind3();

const makeKind4 = make(4, { value: M.string() });
const obj4 = makeKind4('string');

const makeKind5 = make(5, { value: M.remotable() });
const durableValueFail = { message: /value for "value" is not durable/ };
t.throws(() => makeKind5(eph1), durableValueFail);

const durableShapeFail = { message: /stateShape.*is not durable: slot 0 of/ };
t.throws(() => make(6, { value: eph1 }), durableShapeFail);

const makeKind7 = make(7, { value: obj4 }); // obj4 is durable
makeKind7(obj4);
const specificRemotableFail = { message: /kind3.*Must be:.*kind4/ };
t.throws(() => makeKind7(obj3), specificRemotableFail);
});

// durable Kinds maintain refcounts on their serialized stateShape

test('durable stateShape refcounts', async t => {
const kvStore = new Map();
const { syscall: sc1 } = buildSyscall({ kvStore });
const gcTools = makeMockGC();

function build1(vatPowers, _vp, baggage) {
const { VatData } = vatPowers;
const { makeKindHandle, defineDurableKind } = VatData;

return Far('root', {
accept: _standard1 => 0, // assign it a vref
create: standard1 => {
const kh = makeKindHandle('shaped');
baggage.init('kh', kh);
const stateShape = { value: standard1 };
defineDurableKind(kh, init, behavior, { stateShape });
},
});
}

const makeNS1 = () => ({ buildRootObject: build1 });
const ls1 = makeLiveSlots(sc1, 'vatA', {}, {}, gcTools, undefined, makeNS1);
const startVat1 = makeStartVat(kser());
await ls1.dispatch(startVat1);
const rootA = 'o+0';

const standard1Vref = 'o-1';
await ls1.dispatch(makeMessage(rootA, 'accept', []));
t.falsy(ls1.testHooks.getReachableRefCount(standard1Vref));

await ls1.dispatch(makeMessage(rootA, 'create', [kslot(standard1Vref)]));

// using our 'standard1' object in stateShape causes its refcount to
// be incremented
t.is(ls1.testHooks.getReachableRefCount(standard1Vref), 1);

// ------

// Simulate upgrade by starting from the non-empty kvStore.
const clonedStore = new Map(kvStore);
const { syscall: sc2 } = buildSyscall({ kvStore: clonedStore });

function build2(vatPowers, vatParameters, baggage) {
const { VatData } = vatPowers;
const { defineDurableKind } = VatData;
const { standard2 } = vatParameters;
const kh = baggage.get('kh');
const stateShape = { value: standard2 };
defineDurableKind(kh, init, behavior, { stateShape });

return Far('root', {});
}

const makeNS2 = () => ({ buildRootObject: build2 });
const ls2 = makeLiveSlots(sc2, 'vatA', {}, {}, gcTools, undefined, makeNS2);

const standard2Vref = 'o-2';
const vp = { standard2: kslot(standard2Vref) };
const startVat2 = makeStartVat(kser(vp));
await ls2.dispatch(startVat2);

// redefining the durable kind, with a different 'standard' object,
// will decrement the standard1 refcount, and increment that of
// standard2

t.falsy(ls2.testHooks.getReachableRefCount(standard1Vref));
t.is(ls2.testHooks.getReachableRefCount(standard2Vref), 1);
});
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ test('durable kind IDs can be reanimated', t => {
const makeThing = defineDurableKind(fetchedKindID, initThing, thingBehavior);
t.is(
log.shift(),
'set vom.dkind.10 {"kindID":"10","tag":"testkind","nextInstanceID":1,"unfaceted":true}',
'set vom.dkind.10 {"kindID":"10","tag":"testkind","nextInstanceID":1,"unfaceted":true,"stateShapeCapData":{"body":"#\\"#undefined\\"","slots":[]}}',
);
t.deepEqual(log, []);

Expand All @@ -622,7 +622,7 @@ test('durable kind IDs can be reanimated', t => {
flushStateCache();
t.is(
log.shift(),
'set vom.dkind.10 {"kindID":"10","tag":"testkind","nextInstanceID":2,"unfaceted":true}',
'set vom.dkind.10 {"kindID":"10","tag":"testkind","nextInstanceID":2,"unfaceted":true,"stateShapeCapData":{"body":"#\\"#undefined\\"","slots":[]}}',
);
t.is(log.shift(), `set vom.o+d10/1 ${thingVal(0, 'laterThing', 0)}`);
t.deepEqual(log, []);
Expand Down

0 comments on commit 2ee1745

Please sign in to comment.