diff --git a/packages/swingset-liveslots/src/liveslots.js b/packages/swingset-liveslots/src/liveslots.js index b85cc40c40ea..3bf0b0467cd9 100644 --- a/packages/swingset-liveslots/src/liveslots.js +++ b/packages/swingset-liveslots/src/liveslots.js @@ -43,7 +43,7 @@ function build( syscall, forVatID, vatPowers, - liveSlotsOptions, + liveSlotsOptions = {}, gcTools, console, buildVatNamespace, @@ -664,6 +664,7 @@ function build( m.serialize, unmeteredUnserialize, assertAcceptableSyscallCapdataSize, + liveSlotsOptions, ); const collectionManager = makeCollectionManager( diff --git a/packages/swingset-liveslots/src/types.js b/packages/swingset-liveslots/src/types.js index e74e380c8f2f..da7027a27b03 100644 --- a/packages/swingset-liveslots/src/types.js +++ b/packages/swingset-liveslots/src/types.js @@ -22,6 +22,7 @@ * @typedef {{ * enableDisavow?: boolean, * relaxDurabilityRules?: boolean, + * allowStateShapeChanges?: boolean, * }} LiveSlotsOptions * * @typedef {import('@endo/marshal').CapData} SwingSetCapData diff --git a/packages/swingset-liveslots/src/virtualObjectManager.js b/packages/swingset-liveslots/src/virtualObjectManager.js index 63da638dce45..4cb198aea51b 100644 --- a/packages/swingset-liveslots/src/virtualObjectManager.js +++ b/packages/swingset-liveslots/src/virtualObjectManager.js @@ -263,6 +263,20 @@ function insistDurableCapdata(vrm, what, capdata, valueFor) { }); } +function insistSameCapData(oldCD, newCD) { + if (oldCD.body !== newCD.body) { + Fail`durable Kind stateShape mismatch (body)`; + } + if (oldCD.slots.length !== newCD.slots.length) { + Fail`durable Kind stateShape mismatch (slots.length)`; + } + oldCD.slots.forEach((oldVref, idx) => { + if (newCD.slots[idx] !== oldVref) { + Fail`durable Kind stateShape mismatch (slot[${idx}])`; + } + }); +} + /** * Create a new virtual object manager. There is one of these for each vat. * @@ -281,6 +295,7 @@ function insistDurableCapdata(vrm, what, capdata, valueFor) { * @param {import('@endo/marshal').Unserialize} unserialize Unserializer for this vat * @param {*} assertAcceptableSyscallCapdataSize Function to check for oversized * syscall params + * @param {import('./types').LiveSlotsOptions} liveSlotsOptions * * @returns {object} a new virtual object manager. * @@ -321,7 +336,10 @@ export function makeVirtualObjectManager( serialize, unserialize, assertAcceptableSyscallCapdataSize, + liveSlotsOptions = {}, ) { + const { allowStateShapeChanges = false } = liveSlotsOptions; + // array of Caches that need to be flushed at end-of-crank, two per Kind // (dataCache, contextCache) const allCaches = []; @@ -704,22 +722,24 @@ export function makeVirtualObjectManager( // compatibility checkAndUpdateFacetiousness(tag, durableKindDescriptor, facetNames); - const stateShapeCapData = serialize(stateShape); + const newShapeCD = 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); + insistDurableCapdata(vrm, 'stateShape', newShapeCD, false); // compare against slots of previous definition, incref/decref - let oldStateShapeSlots = []; - if (durableKindDescriptor.stateShapeCapData) { - oldStateShapeSlots = durableKindDescriptor.stateShapeCapData.slots; + const oldShapeCD = durableKindDescriptor.stateShapeCapData; + + const oldStateShapeSlots = oldShapeCD ? oldShapeCD.slots : []; + if (oldShapeCD && !allowStateShapeChanges) { + insistSameCapData(oldShapeCD, newShapeCD); } - const newStateShapeSlots = stateShapeCapData.slots; + const newStateShapeSlots = newShapeCD.slots; vrm.updateReferenceCounts(oldStateShapeSlots, newStateShapeSlots); - durableKindDescriptor.stateShapeCapData = stateShapeCapData; // replace + durableKindDescriptor.stateShapeCapData = newShapeCD; // replace saveDurableKindDescriptor(durableKindDescriptor); } diff --git a/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js b/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js index 19a2446bcbfb..e8af616a3be7 100644 --- a/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js +++ b/packages/swingset-liveslots/test/virtual-objects/test-state-shape.js @@ -182,8 +182,20 @@ test('durable stateShape refcounts', async t => { return Far('root', {}); } + // to test refcount increment/decrement, we need to override the + // usual rule that the new version must exactly match the original + // stateShape + const options = { allowStateShapeChanges: true }; const makeNS2 = () => ({ buildRootObject: build2 }); - const ls2 = makeLiveSlots(sc2, 'vatA', {}, {}, gcTools, undefined, makeNS2); + const ls2 = makeLiveSlots( + sc2, + 'vatA', + {}, + options, + gcTools, + undefined, + makeNS2, + ); const standard2Vref = 'o-2'; const vp = { standard2: kslot(standard2Vref) }; @@ -197,3 +209,90 @@ test('durable stateShape refcounts', async t => { t.falsy(ls2.testHooks.getReachableRefCount(standard1Vref)); t.is(ls2.testHooks.getReachableRefCount(standard2Vref), 1); }); + +test('durable stateShape must match', 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', { + create: (obj1, obj2) => { + const kh = makeKindHandle('shaped'); + baggage.init('kh', kh); + const stateShape = { x: obj1, y: obj2 }; + 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 vref1 = 'o-1'; + const vref2 = 'o-2'; + await ls1.dispatch( + makeMessage(rootA, 'create', [kslot(vref1), kslot(vref2)]), + ); + + // the first version's state is { x: vref1, y: vref2 } + + // ------ + + // 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 { obj1, obj2 } = vatParameters; + const kh = baggage.get('kh'); + // several shapes that are not compatible + const shape1 = { x: obj1, y: M.any() }; + const shape2 = { x: obj1 }; + const shape3 = { x: obj1, y: obj2, z: M.string() }; + const shape4 = { x: M.or(obj1, M.string()), y: obj2 }; + const shape5 = { x: obj2, y: obj1 }; // wrong slots + const trial = shape => { + t.throws( + () => defineDurableKind(kh, init, behavior, { stateShape: shape }), + { message: /durable Kind stateShape mismatch/ }, + ); + }; + trial(shape1); + trial(shape2); + trial(shape3); + trial(shape4); + trial(shape5); + const stateShape = { x: obj1, y: obj2 }; // the correct shape + defineDurableKind(kh, init, behavior, { stateShape }); + t.pass(); + + return Far('root', {}); + } + + // we do *not* override allowStateShapeChanges + // const options = { allowStateShapeChanges: true }; + const options = undefined; + const makeNS2 = () => ({ buildRootObject: build2 }); + const ls2 = makeLiveSlots( + sc2, + 'vatA', + {}, + options, + gcTools, + undefined, + makeNS2, + ); + + const vp = { obj1: kslot(vref1), obj2: kslot(vref2) }; + const startVat2 = makeStartVat(kser(vp)); + await ls2.dispatch(startVat2); +});