diff --git a/packages/swingset-liveslots/src/virtualObjectManager.js b/packages/swingset-liveslots/src/virtualObjectManager.js index 9859ea07d84..63da638dce4 100644 --- a/packages/swingset-liveslots/src/virtualObjectManager.js +++ b/packages/swingset-liveslots/src/virtualObjectManager.js @@ -146,15 +146,10 @@ 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) { @@ -162,8 +157,7 @@ function checkAndUpdateFacetiousness( } else { desc.unfaceted = true; } - saveDurableKindDescriptor(desc); - return; + return; // caller will saveDurableKindDescriptor() } // When a later incarnation redefines the behavior, it must match. @@ -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} serialize Serializer for this vat - * @param {import('@endo/marshal').Unserialize} unserialize Unserializer for this vat + * @param {import('@endo/marshal').Serialize} serialize Serializer for this vat + * @param {import('@endo/marshal').Unserialize} unserialize Unserializer for this vat * @param {*} assertAcceptableSyscallCapdataSize Function to check for oversized * syscall params * @@ -533,6 +527,7 @@ export function makeVirtualObjectManager( * tag: string, * unfaceted?: boolean, * facets?: string[], + * stateShapeCapData?: import('./types.js').SwingSetCapData * }} DurableKindDescriptor */ @@ -680,14 +675,14 @@ 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) can both check for compatibility, and + // to decrement refcounts on any slots referenced by the old + // shape. harden(stateShape); stateShape === undefined || @@ -695,6 +690,40 @@ export function makeVirtualObjectManager( 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; diff --git a/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js b/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js new file mode 100644 index 00000000000..19a2446bcbf --- /dev/null +++ b/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js @@ -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); +}); diff --git a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js index 00e8eab7aaa..f1ed2f763f5 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-virtualObjectManager.js @@ -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, []); @@ -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, []);