Skip to content

Commit 4841c87

Browse files
authored
feat(swingset-liveslots): Support adding optional top-level fields to stateShape records (#11216)
Fixes #10200 ## Description Updates the liveslots virtual object manager (VOM) to allow a new incarnation to add top-level fields to an existing kind stateShape, as long as they are clearly optional (consisting of a ["match:or" Pattern](https://endojs.github.io/endo/interfaces/_endo_patterns.PatternMatchers.html#or) in which at least one alternative is `undefined`) and regardless of `allowStateShapeChanges` configuration (i.e., they are allowed even when that setting is false, because they are backwards compatible). ### Security Considerations Any code specifying a stateShape in which a field can be undefined is responsible for accommodating old values in which that field _is_ undefined. The VOM state record field accessors are also updated to tolerate values missing in the backing store, but such a change is safe because those accessors only exist for known fields. ### Scaling Considerations `buildRootObject` will be slower when the new stateShape does not match the old, because we will now be deserializing the old and serializing the old and new patterns for comparison. But stateShape records are not expected to be large. ### Documentation Considerations None known. ### Testing Considerations packages/swingset-liveslots/test/virtual-objects/state-shape.test.js should provide sufficient coverage, although exo-specific testing could also be added. ### Upgrade Considerations New vat code can only take advantage of this with an updated liveslots, which should happen as part of vat upgrade anyway.
2 parents 0282f99 + 371010e commit 4841c87

File tree

5 files changed

+468
-242
lines changed

5 files changed

+468
-242
lines changed

packages/swingset-liveslots/src/types.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ export {};
2222
*/
2323

2424
/**
25-
* @typedef {{
26-
* enableDisavow?: boolean,
27-
* relaxDurabilityRules?: boolean,
28-
* allowStateShapeChanges?: boolean,
29-
* }} LiveSlotsOptions
25+
* @typedef {object} LiveSlotsOptions
26+
* @property {boolean} [enableDisavow]
27+
* @property {boolean} [relaxDurabilityRules]
28+
* @property {boolean} [allowStateShapeChanges] somewhat misnamed; this actually
29+
* relates to *arbitrary* changes (per #10200, a non-true value still permits
30+
* backwards-compatible addition of new optional fields)
3031
*
3132
* @typedef {import('@endo/marshal').CapData<string>} SwingSetCapData
3233
*

packages/swingset-liveslots/src/virtualObjectManager.js

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { assertPattern, mustMatch } from '@agoric/store';
77
import { defendPrototype, defendPrototypeKit } from '@endo/exo/tools.js';
88
import { Far, passStyleOf } from '@endo/marshal';
99
import { Nat } from '@endo/nat';
10+
import { kindOf } from '@endo/patterns';
1011
import { parseVatSlot, makeBaseRef } from './parseVatSlots.js';
1112
import { enumerateKeysWithPrefix } from './vatstore-iterators.js';
1213
import { makeCache } from './cache.js';
@@ -20,6 +21,8 @@ import {
2021
* @import {DefineKindOptions} from '@agoric/swingset-liveslots'
2122
* @import {ClassContextProvider, KitContextProvider} from '@endo/exo'
2223
* @import {ToCapData, FromCapData} from '@endo/marshal';
24+
* @import {Pattern} from '@endo/patterns';
25+
* @import {SwingSetCapData} from './types.js';
2326
*/
2427

2528
const {
@@ -241,12 +244,47 @@ const insistDurableCapdata = (vrm, what, capdata, valueFor) => {
241244
}
242245
};
243246

244-
const insistSameCapData = (oldCD, newCD) => {
245-
// NOTE: this assumes both were marshalled with the same format
246-
// (e.g. smallcaps vs pre-smallcaps). To somewhat tolerate new
247-
// formats, we'd need to `serialize(unserialize(oldCD))`.
247+
const isUndefinedPatt = patt =>
248+
patt === undefined ||
249+
(kindOf(patt) === 'match:kind' && patt.payload === 'undefined');
250+
251+
/**
252+
* Assert that a new stateShape either matches the old, or only differs in the
253+
* addition of new clearly-optional top-level fields as conveyed by an
254+
* [Endo `M.or`]{@link https://endojs.github.io/endo/interfaces/_endo_patterns.PatternMatchers.html#or}
255+
* Pattern with at least one `undefined` alternative.
256+
*
257+
* @param {SwingSetCapData} oldCD
258+
* @param {SwingSetCapData} newCD
259+
* @param {{ newShape: Record<string, Pattern>, serialize: ToCapData<string>, unserialize: FromCapData<string> }} powers
260+
*/
261+
const insistCompatibleShapeCapData = (
262+
oldCD,
263+
newCD,
264+
{ newShape, serialize, unserialize },
265+
) => {
248266
if (oldCD.body !== newCD.body) {
249-
Fail`durable Kind stateShape mismatch (body)`;
267+
// Allow introduction of any clearly-optional new field at top level.
268+
const oldShape =
269+
unserialize(oldCD) ||
270+
Fail`durable Kind stateShape mismatch (no old shape)`;
271+
passStyleOf(oldShape) === 'copyRecord' ||
272+
Fail`durable Kind stateShape mismatch (invalid old shape)`;
273+
assertPattern(oldShape);
274+
for (const [name, oldPatt] of Object.entries(oldShape)) {
275+
// Assert presence and CapData shape, but save slots for their own clause.
276+
(Object.hasOwn(newShape, name) &&
277+
serialize(newShape[name]).body === serialize(oldPatt).body) ||
278+
Fail`durable Kind stateShape mismatch (body ${name})`;
279+
}
280+
for (const [name, newPatt] of Object.entries(newShape)) {
281+
if (Object.hasOwn(oldShape, name)) continue;
282+
kindOf(newPatt) === 'match:or' ||
283+
Fail`durable Kind stateShape mismatch (body new field ${name})`;
284+
// @ts-expect-error A "match:or" pattern has a `payload` array of Patterns
285+
newPatt.payload.some(patt => isUndefinedPatt(patt)) ||
286+
Fail`durable Kind stateShape mismatch (body ${name})`;
287+
}
250288
}
251289
if (oldCD.slots.length !== newCD.slots.length) {
252290
Fail`durable Kind stateShape mismatch (slots.length)`;
@@ -532,7 +570,7 @@ export const makeVirtualObjectManager = (
532570
* tag: string,
533571
* unfaceted?: boolean,
534572
* facets?: string[],
535-
* stateShapeCapData?: import('./types.js').SwingSetCapData
573+
* stateShapeCapData?: SwingSetCapData
536574
* }} DurableKindDescriptor
537575
*/
538576

@@ -803,7 +841,11 @@ export const makeVirtualObjectManager = (
803841

804842
const oldStateShapeSlots = oldShapeCD ? oldShapeCD.slots : [];
805843
if (oldShapeCD && !allowStateShapeChanges) {
806-
insistSameCapData(oldShapeCD, newShapeCD);
844+
insistCompatibleShapeCapData(oldShapeCD, newShapeCD, {
845+
newShape: /** @type {Record<string, Pattern>} */ (stateShape),
846+
serialize,
847+
unserialize,
848+
});
807849
}
808850
const newStateShapeSlots = newShapeCD.slots;
809851
vrm.updateReferenceCounts(oldStateShapeSlots, newStateShapeSlots);
@@ -859,9 +901,11 @@ export const makeVirtualObjectManager = (
859901
const baseRef = getBaseRef(this);
860902
const record = dataCache.get(baseRef);
861903
assert(record !== undefined);
862-
const { valueMap, capdatas } = record;
904+
const { capdatas, valueMap } = record;
863905
if (!valueMap.has(prop)) {
864-
const value = harden(unserialize(capdatas[prop]));
906+
const value = hasOwn(capdatas, prop)
907+
? harden(unserialize(capdatas[prop]))
908+
: undefined;
865909
checkStatePropertyValue(value, prop);
866910
valueMap.set(prop, value);
867911
}
@@ -877,12 +921,19 @@ export const makeVirtualObjectManager = (
877921
}
878922
const record = dataCache.get(baseRef); // mutable
879923
assert(record !== undefined);
880-
const oldSlots = record.capdatas[prop].slots;
924+
const { capdatas, valueMap } = record;
925+
const oldSlots = hasOwn(capdatas, prop) ? capdatas[prop].slots : [];
881926
const newSlots = capdata.slots;
882927
vrm.updateReferenceCounts(oldSlots, newSlots);
883-
record.capdatas[prop] = capdata; // modify in place ..
884-
record.valueMap.set(prop, value);
885-
dataCache.set(baseRef, record); // .. but mark as dirty
928+
// modify in place, but mark as dirty
929+
defineProperty(capdatas, prop, {
930+
value: capdata,
931+
writable: true,
932+
enumerable: true,
933+
configurable: false,
934+
});
935+
valueMap.set(prop, value);
936+
dataCache.set(baseRef, record);
886937
},
887938
enumerable: true,
888939
configurable: false,
@@ -1073,7 +1124,12 @@ export const makeVirtualObjectManager = (
10731124
}
10741125
// eslint-disable-next-line github/array-foreach
10751126
valueCD.slots.forEach(vrm.addReachableVref);
1076-
capdatas[prop] = valueCD;
1127+
defineProperty(capdatas, prop, {
1128+
value: valueCD,
1129+
writable: true,
1130+
enumerable: true,
1131+
configurable: false,
1132+
});
10771133
valueMap.set(prop, value);
10781134
}
10791135
// dataCache contents remain mutable: state setter modifies in-place

packages/swingset-liveslots/test/util.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ export function makeMessage(target, method, args = [], result = null) {
5959
return vatDeliverObject;
6060
}
6161

62-
export function makeStartVat(vatParameters) {
63-
return harden(['startVat', vatParameters]);
62+
export function makeStartVat(vatParametersCapData = kser(undefined)) {
63+
return harden(['startVat', vatParametersCapData]);
6464
}
6565

6666
export function makeBringOutYourDead() {

0 commit comments

Comments
 (0)