From 3e02d42312b2963c165623c8cd559b431e5ecdce Mon Sep 17 00:00:00 2001 From: Chip Morningstar Date: Mon, 28 Mar 2022 19:36:54 -0700 Subject: [PATCH] feat: yet another overhaul of the `defineKind` API Closes #4905 --- packages/ERTP/src/payment.js | 14 +- packages/ERTP/src/purse.js | 39 +- packages/SwingSet/docs/virtual-objects.md | 90 +-- .../src/liveslots/collectionManager.js | 20 +- packages/SwingSet/src/liveslots/liveslots.js | 12 +- .../src/liveslots/virtualObjectManager.js | 237 +++++--- .../src/liveslots/virtualReferences.js | 47 +- .../test/stores/test-durabilityChecks.js | 10 +- packages/SwingSet/test/test-vat-env.js | 8 +- packages/SwingSet/test/upgrade/vat-ulrik-1.js | 22 +- packages/SwingSet/test/upgrade/vat-ulrik-2.js | 21 +- .../double-retire-import/vat-dri.js | 20 +- .../virtualObjects/test-reachable-vrefs.js | 22 +- .../virtualObjects/test-representatives.js | 54 +- .../virtualObjects/test-retain-remotable.js | 22 +- .../virtualObjects/test-virtualObjectGC.js | 50 +- .../test-virtualObjectManager.js | 158 +++-- .../test/virtualObjects/vat-orphan-bob.js | 129 +++- .../virtualObjects/vat-orphan-bootstrap.js | 7 +- .../vat-representative-bootstrap.js | 8 +- .../test/virtualObjects/vat-vom-gc-bob.js | 10 +- .../vat-weakcollections-alice.js | 16 +- packages/SwingSet/tools/fakeVirtualSupport.js | 5 +- .../run-protocol/src/vaultFactory/vault.js | 549 ++++++++++-------- .../swingset-runner/demo/vatStore1/vat-bob.js | 24 +- .../demo/vatStore2/thingHolder.js | 12 +- .../swingset-runner/demo/vatStore3/vat-bob.js | 10 +- .../demo/virtualObjectGC/vat-bob.js | 20 +- packages/vat-data/src/index.test-d.ts | 44 ++ packages/vat-data/src/types.d.ts | 19 +- packages/vat-data/test/present.test.js | 6 +- packages/zoe/test/minimalMakeKindContract.js | 4 +- packages/zoe/test/unitTests/test-makeKind.js | 4 +- packages/zoe/test/unitTests/test-zoe-env.js | 4 +- 34 files changed, 965 insertions(+), 752 deletions(-) diff --git a/packages/ERTP/src/payment.js b/packages/ERTP/src/payment.js index 0a057b3d40d..ff63ee45ff2 100644 --- a/packages/ERTP/src/payment.js +++ b/packages/ERTP/src/payment.js @@ -9,13 +9,11 @@ import { defineKind } from '@agoric/vat-data'; * @returns {() => Payment} */ export const makePaymentMaker = (allegedName, brand) => { - const makePayment = defineKind( - `${allegedName} payment`, - () => ({}), - () => ({ - getAllegedBrand: () => brand, - }), - ); - return makePayment; + const makePayment = defineKind(`${allegedName} payment`, () => ({}), { + getAllegedBrand: () => brand, + }); + // XXX the following type cast is meatball surgery to make tsc shut up + // somebody who understands this should do it properly + return /** @type {() => Payment} */ (makePayment); }; harden(makePaymentMaker); diff --git a/packages/ERTP/src/purse.js b/packages/ERTP/src/purse.js index c99d8cba680..88c8041ea02 100644 --- a/packages/ERTP/src/purse.js +++ b/packages/ERTP/src/purse.js @@ -3,6 +3,11 @@ import { defineKind } from '@agoric/vat-data'; import { AmountMath } from './amountMath.js'; export const makePurseMaker = (allegedName, assetKind, brand, purseMethods) => { + const updatePurseBalance = (state, newPurseBalance) => { + state.currentBalance = newPurseBalance; + state.balanceUpdater.updateState(state.currentBalance); + }; + // - This kind is a pair of purse and depositFacet that have a 1:1 // correspondence. // - They are virtualized together to share a single state record. @@ -24,41 +29,33 @@ export const makePurseMaker = (allegedName, assetKind, brand, purseMethods) => { balanceUpdater, }; }, - state => { - const { balanceNotifier, balanceUpdater } = state; - const updatePurseBalance = newPurseBalance => { - state.currentBalance = newPurseBalance; - balanceUpdater.updateState(state.currentBalance); - }; - - /** @type {Purse} */ - const purse = { - deposit: (srcPayment, optAmountShape = undefined) => { + { + purse: { + deposit: ({ state }, srcPayment, optAmountShape = undefined) => { // Note COMMIT POINT within deposit. return purseMethods.deposit( state.currentBalance, - updatePurseBalance, + newPurseBalance => updatePurseBalance(state, newPurseBalance), srcPayment, optAmountShape, ); }, - withdraw: amount => + withdraw: ({ state }, amount) => // Note COMMIT POINT within withdraw. purseMethods.withdraw( state.currentBalance, - updatePurseBalance, + newPurseBalance => updatePurseBalance(state, newPurseBalance), amount, ), - getCurrentAmount: () => state.currentBalance, - getCurrentAmountNotifier: () => balanceNotifier, + getCurrentAmount: ({ state }) => state.currentBalance, + getCurrentAmountNotifier: ({ state }) => state.balanceNotifier, getAllegedBrand: () => brand, // eslint-disable-next-line no-use-before-define - getDepositFacet: () => depositFacet, - }; - const depositFacet = { - receive: purse.deposit, - }; - return { purse, depositFacet }; + getDepositFacet: ({ facets }) => facets.depositFacet, + }, + depositFacet: { + receive: ({ facets }, ...args) => facets.purse.deposit(...args), + }, }, ); return () => makePurseKit().purse; diff --git a/packages/SwingSet/docs/virtual-objects.md b/packages/SwingSet/docs/virtual-objects.md index 449b2e21b44..feb336e0af1 100644 --- a/packages/SwingSet/docs/virtual-objects.md +++ b/packages/SwingSet/docs/virtual-objects.md @@ -19,9 +19,9 @@ A VDO has a "kind", which defines what sort of behavior and state it will posses A vat can define new kinds of VDOs by calling the `defineKind` or `defineDurableKind` functions: - `maker = defineKind(descriptionTag, init, actualize, finish)` + `maker = defineKind(descriptionTag, init, behavior, options)` or - `maker = defineDurableKind(kindHandle, init, actual, finish)` + `maker = defineDurableKind(kindHandle, init, behavior, options)` The return value from `defineKind` or `defineDurableKind` is a maker function which the vat can use to create instances of the newly defined VDO kind. @@ -31,50 +31,56 @@ A `kindHandle` is a type of durable object that can be used to identify the kind `kindHandle = makeKindHandle(descriptionTag)` -where `descriptionTag` is exactly the same as the same named parameter of `defineKind`. The difference is that a kind handle is itself that a durable object that may be stored for later retrieval, and used in a future call to `defineDurableKind` to associate new behavior with the kind in question. +where `descriptionTag` is exactly the same as the same named parameter of `defineKind`. The difference is that a kind handle is itself a durable object that may be stored for later retrieval, and used in a future call to `defineDurableKind` to associate new behavior with the kind in question. The `init` parameter is a function that will be called when new instances are first created. It is expected to return a simple JavaScript object that represents the initialized state for the new VDO instance. Any parameters passed to the maker function returned by `defineKind`/`defineDurableKind` are passed directly to the `init` function. -The `actualize` parameter is a function that binds an in-memory instance (the "Representative") of the VDO with the VDO's state, associating such instances with the VDO's behavior. It is passed the VDO's state as a parameter and is expected to return either: +The `behavior` parameter is an object that describes the VDO's behavior. It must take one of two forms: -1. A new JavaScript object with methods that close over the given state. This returned object will become the body of the new instance. This object can be empty; in such a case it can serve as a powerless but unforgeable "marker" handle. +1. An object whose named properties are all functions that will become methods of the virtual objects returned by the maker function. The behavior object can be empty; in such a case the resulting VDO can serve as a powerless but unforgeable "marker" handle. -2. A new JavaScript object populated with objects as described in (1). These will become facets of the new instance. The returned object will be an object mapping to the facets by name. +2. An object whose named properties are objects as described in (1). These will become facets of new instances of the VDO. The return value from the maker function object will be an object mapping to the facets by name. -The `actualize` function is called whenever a new VDO instance is created, whenever such an instance is swapped in from secondary storage, and whenever a reference to a VDO is received as a parameter of a message and deserialized. Note that for any given VDO kind, the shape of the value returned by the `actualize` function may not vary over successive calls. That is, if it's a single facet, it must always be a single facet, and if it's multiple facets it must always be the same set of multiple facets. +In either case, the individual behavior functions must have the signature: -The `finish` parameter is optional. It is a function that, if present, will be called exactly once as part of instance initialization. It will be invoked _immediately_ after the `actualize` function for that instance is called for the very first time. In other words, it will be called after the instance per se exists but before that instance is returned from the maker function to whoever requested its creation. `finish` is passed two parameters: the VDO's state (exactly as passed to the `actualize` function) and the VDO itself. The `finish` function can modify the object's state in the context of knowing the object's identity, and thus can be used in cases where a validly initialized instance requires it to participate in some kind of cyclical object graph with other VDOs. It can also be used, for example, to register the object with outside tracking data structures, or do whatever other post-creation setup is needed for the object to do its job. In particular, if one or more of the object's methods need to refer to the object itself (for example, so it can pass itself to other objects), the `finish` function provides a way to capture that identity as part of the object's state. + `methodname(context, ...args) { ...` + +where `context` describes the invocation context of the method and `...args` are whatever arguments were passed to the method when it was invoked. In the case of a single facet VDO, `context` will take the form `{ state, self }`, where `state` is the VDO state and `self` is a reference to the VDO itself. In the case of a multi-facet VDO, `context` will instead be `{ state, facets }`, where `facets` is an object whose named properties are the facets of the VDO. + +The `options` parameter is optional. It provides additional parameters to characterize the VDO. Currently there is only one supported option, `finish`, though we anticipate more options may be added in future versions of the API. + +The `finish` option is a function that, if present, will be called at the end of instance initialization. It will be invoked after the VDO is created but before it is returned from the maker function. `finish` is passed one parameters, a `context` object identical to that passed to method behaviors. The `finish` function can modify the object's state at a time when the object's identity is known (or its facets' identies are known), and thus can be used in cases where a validly initialized instance requires it to participate in some kind of cyclical object graph with other VDOs. It can also be used, for example, to register the object with outside tracking data structures, or do whatever other post-creation setup is needed for the object to do its job. For example: ```javascript const initCounter = (name) => ({ counter: 0, name }); - const actualizeCounter = (state) => ({ - inc: () => { + const counterBehavior = { + inc: ({state}) => { state.counter += 1; }, - dec: () => { + dec: ({state}) => { state.counter -= 1; }, - reset: () => { + reset: ({state}) => { state.counter = 0; }, - rename: (newName) => { + rename: ({state}, newName) => { state.name = newName; }, - getCount: () => state.counter, - getName: () => state.name, - }); + getCount: ({state}) => state.counter, + getName: ({state}) => state.name, + }; const finishCounter = (state, counter) => { addToCounterRegistry(counter, state.name); }; - const makeCounter = defineKind('counter', initCounter, actualizeCounter, finishCounter); + const makeCounter = defineKind('counter', initCounter, counterBehavior, { finish: finishCounter }); ``` -This defines a simple virtual counter object with two properties in its state, a count and a name. You'd use it like this: +This defines a simple virtual counter object with two properties in its state: a count and a name. Note that none of the methods bother to declare the `self` context parameter because none of them need to refer to it. You'd use it like this: ```javascript const fooCounter = makeCounter('foo'); @@ -93,33 +99,33 @@ Suppose you instead wanted to provide a version with the increment and decrement ```javascript const initFacetedCounter = () => ({ counter: 0 }); - const actualizeFacetedCounter = (state) => { - const getCount = () => state.counter; - return { - incr: { - step: () => { - state.counter += 1; - }, - getCount, + const getCount = ({state}) => state.counter, + + const facetedCounterBehavior = { + incr: { + step: ({ state }) => { + state.counter += 1; }, - decr: { - step: () => { - state.counter -= 1; - }, - getCount, + getCount, + }, + decr: { + step: ({ state }) => { + state.counter -= 1; }, - }; - } + getCount, + }, + }; - const makeFacetedCounter = defineKind('counter', initCounter, actualizeCounter); + const makeFacetedCounter = defineKind('counter', initFacetedCounter, facetedCounterBehavior); ``` -If you wanted to also make this durable, instead of the last line you'd generate -the kind with something more like: +Note how the `getCount` method is declared once and then used in two different facets. + +If you wanted to also make this durable, instead of the last line you'd generate the kind with something more like: ```javascript const facetedCounterKind = makeKindHandle('durable counter'); - const makeFacetedCounter = defineDurableKind(facetedCounterKind, initCounter, actualizeCounter); + const makeFacetedCounter = defineDurableKind(facetedCounterKind, initCounter, facetedCounterBehavior); ``` In either case you'd use it like: @@ -134,23 +140,23 @@ In either case you'd use it like: console.log(`count is ${incr.getCount()`); // "count is 1" ``` -Note that the `init`, `actualize`, and `finish` functions are defined explicitly in the above examples for clarity of exposition, but in practice you'd usually declare them inline in the parameters of the `defineKind` call: +Note that the `init` and `finish` functions, as well as the behavior, are defined explicitly in the above examples for clarity of exposition, but in practice you'd usually declare them inline in the parameters of the `defineKind` call: ```javascript + const getCount = ({state}) => state.counter; const makeFacetedCounter = defineKind( 'counter', () => ({ counter: 0 }), (state) => { - const getCount = () => state.counter; return { incr: { - step: () => { + step: ({state}) => { state.counter += 1; }, getCount, }, decr: { - step: () => { + step: ({state}) => { state.counter -= 1; }, getCount, @@ -162,7 +168,7 @@ Note that the `init`, `actualize`, and `finish` functions are defined explicitly Additional important details: -- The set of state properties of an instance is fully determined by the `init` function. That is, the set of properties that exist on in instance's `state` is completely determined by the enumerable properties of the object that `init` returns. State properties cannot thereafter be added or removed. Currently there is no requirement that all instances of a given kind have the same set of properties, but code authors should not rely on this as such enforcement may be added in the future. +- The set of state properties of an instance is fully determined by the `init` function. That is, the set of properties present in instance's `state` is completely determined by the named enumerable properties of the object that `init` returns. State properties cannot thereafter be added or removed from the instance. Currently there is no requirement that all instances of a given kind have the same set of properties, but code authors should not rely on this as such enforcement may be added in the future. - The values a state property may take are limited to things that are serializable and which may be hardened (and, in fact, _are_ hardened and serialized the moment they are assigned). That is, you can replace what value a state property _has_, but you cannot modify a state property's value in situ. In other words, you can do things like: diff --git a/packages/SwingSet/src/liveslots/collectionManager.js b/packages/SwingSet/src/liveslots/collectionManager.js index a2beb8ba8a9..a6756036a14 100644 --- a/packages/SwingSet/src/liveslots/collectionManager.js +++ b/packages/SwingSet/src/liveslots/collectionManager.js @@ -806,24 +806,20 @@ export function makeCollectionManager( ); } - function reanimateScalarMapStore(vobjID, proForma) { - return proForma ? null : collectionToMapStore(reanimateCollection(vobjID)); + function reanimateScalarMapStore(vobjID) { + return collectionToMapStore(reanimateCollection(vobjID)); } - function reanimateScalarWeakMapStore(vobjID, proForma) { - return proForma - ? null - : collectionToWeakMapStore(reanimateCollection(vobjID)); + function reanimateScalarWeakMapStore(vobjID) { + return collectionToWeakMapStore(reanimateCollection(vobjID)); } - function reanimateScalarSetStore(vobjID, proForma) { - return proForma ? null : collectionToSetStore(reanimateCollection(vobjID)); + function reanimateScalarSetStore(vobjID) { + return collectionToSetStore(reanimateCollection(vobjID)); } - function reanimateScalarWeakSetStore(vobjID, proForma) { - return proForma - ? null - : collectionToWeakSetStore(reanimateCollection(vobjID)); + function reanimateScalarWeakSetStore(vobjID) { + return collectionToWeakSetStore(reanimateCollection(vobjID)); } const testHooks = { obtainStoreKindID, storeSizeInternal, makeCollection }; diff --git a/packages/SwingSet/src/liveslots/liveslots.js b/packages/SwingSet/src/liveslots/liveslots.js index 73c54d415a2..87eb951e796 100644 --- a/packages/SwingSet/src/liveslots/liveslots.js +++ b/packages/SwingSet/src/liveslots/liveslots.js @@ -690,16 +690,6 @@ function build( let val = getValForSlot(baseRef); if (val) { if (virtual) { - // If it's a virtual object for which we already have a representative, - // we are going to use that existing representative to preserve === - // equality and WeakMap key usability, BUT we are going to ask the user - // code to make a new representative anyway (which we'll discard) so - // that as far as the user code is concerned we are making a new - // representative with each act of deserialization. This way they can't - // detect reanimation by playing games inside their kind definition to - // try to observe when new representatives are created (e.g., by - // counting calls or squirreling things away in hidden WeakMaps). - vrm.reanimate(baseRef, true); // N.b.: throwing away the result if (facet !== undefined) { return val[facet]; } @@ -709,7 +699,7 @@ function build( let result; if (virtual) { assert.equal(type, 'object'); - val = vrm.reanimate(baseRef, false); + val = vrm.reanimate(baseRef); if (facet !== undefined) { result = val[facet]; } diff --git a/packages/SwingSet/src/liveslots/virtualObjectManager.js b/packages/SwingSet/src/liveslots/virtualObjectManager.js index b079602bb2c..780136a82af 100644 --- a/packages/SwingSet/src/liveslots/virtualObjectManager.js +++ b/packages/SwingSet/src/liveslots/virtualObjectManager.js @@ -7,6 +7,14 @@ import { parseVatSlot } from '../lib/parseVatSlots.js'; // import { kdebug } from './kdebug.js'; +// Marker associated to flag objects that should be held onto strongly if +// somebody attempts to use them as keys in a VirtualObjectAwareWeakSet or +// VirtualObjectAwareWeakMap, despite the fact that keys in such collections are +// nominally held onto weakly. This to thwart attempts to observe GC by +// squirreling away a piece of a VO while the rest of the VO gets GC'd and then +// later regenerated. +const unweakable = new WeakSet(); + /** * Make a simple LRU cache of virtual object inner selves. * @@ -183,11 +191,12 @@ export function makeVirtualObjectManager( ) { const cache = makeCache(cacheSize, fetch, store); - // WeakMap from VO states to VO representatives, to prevent anyone who retains - // a state object from being able to observe the comings and goings of - // representatives. - const stateToRepresentative = new WeakMap(); - const facetToCohort = new WeakMap(); + // WeakMap tieing VO components together, to prevent anyone who retains one + // piece (say, the state object) from being able to observe the comings and + // goings of representatives by hanging onto that piece while the other pieces + // are GC'd, then comparing it to what gets generated when the VO is + // reconstructed by a later import. + const linkToCohort = new WeakMap(); /** * Fetch an object's state from secondary storage. @@ -216,6 +225,31 @@ export function makeVirtualObjectManager( syscall.vatstoreSet(`vom.${baseRef}`, JSON.stringify(rawState)); } + // This is a WeakMap from VO aware weak collections to strong Sets that retain + // keys used in the associated collection that should not actually be held + // weakly. + const unweakableKeySets = new WeakMap(); + + function preserveUnweakableKey(collection, key) { + if (unweakable.has(key)) { + let uwkeys = unweakableKeySets.get(collection); + if (!uwkeys) { + uwkeys = new Set(); + unweakableKeySets.set(collection, uwkeys); + } + uwkeys.add(key); + } + } + + function releaseUnweakableKey(collection, key) { + if (unweakable.has(key)) { + const uwkeys = unweakableKeySets.get(collection); + if (uwkeys) { + uwkeys.delete(key); + } + } + } + /* eslint max-classes-per-file: ["error", 2] */ const actualWeakMaps = new WeakMap(); @@ -265,6 +299,7 @@ export function makeVirtualObjectManager( } vmap.set(vkey, value); } else { + preserveUnweakableKey(this, key); actualWeakMaps.get(this).set(key, value); } return this; @@ -281,6 +316,7 @@ export function makeVirtualObjectManager( return false; } } else { + releaseUnweakableKey(this, key); return actualWeakMaps.get(this).delete(key); } } @@ -332,6 +368,7 @@ export function makeVirtualObjectManager( vset.add(vkey); } } else { + preserveUnweakableKey(this, value); actualWeakSets.get(this).add(value); } return this; @@ -348,6 +385,7 @@ export function makeVirtualObjectManager( return false; } } else { + releaseUnweakableKey(this, value); return actualWeakSets.get(this).delete(value); } } @@ -411,6 +449,26 @@ export function makeVirtualObjectManager( } } + function copyMethods(behavior) { + const obj = {}; + for (const [name, func] of Object.entries(behavior)) { + assert.typeof(func, 'function'); + obj[name] = func; + } + return obj; + } + + function bindMethods(context, behaviorTemplate) { + const obj = {}; + for (const [name, func] of Object.entries(behaviorTemplate)) { + assert.typeof(func, 'function'); + const method = (...args) => Reflect.apply(func, null, [context, ...args]); + unweakable.add(method); + obj[name] = method; + } + return obj; + } + /** * Define a new kind of virtual object. * @@ -421,13 +479,14 @@ export function makeVirtualObjectManager( * @param {*} init An initialization function that will return the initial * state of a new instance of the kind of virtual object being defined. * - * @param {*} actualize An actualization function that will provide the - * in-memory representative object that wraps behavior around the - * virtualized state of an instance of the object kind being defined. + * @param {*} behavior A bag of functions (in the case of a single-faceted + * object) or a bag of bags of functions (in the case of a multi-faceted + * object) that will become the methods of the object or its facets. * - * @param {*} finish An optional finisher function that can perform - * post-creation initialization operations, such as inserting the new - * object in a cyclical object graph. + * @param {*} options Additional options to configure the virtual object kind + * being defined. Currently the only supported option is `finish`, an + * optional finisher function that can perform post-creation initialization + * operations, such as inserting the new object in a cyclical object graph. * * @param {boolean} durable A flag indicating whether or not the newly defined * kind should be a durable kind. @@ -496,17 +555,46 @@ export function makeVirtualObjectManager( * reference to the state is nulled out and the object holding the state * becomes garbage collectable. */ - function defineKindInternal(kindID, tag, init, actualize, finish, durable) { + function defineKindInternal(kindID, tag, init, behavior, options, durable) { + const finish = options ? options.finish : undefined; let nextInstanceID = 1; - - function makeRepresentative(innerSelf, initializing, proForma) { - if (!proForma) { + let facetNames; + let behaviorTemplate; + + const facetiousness = assessFacetiousness(behavior); + switch (facetiousness) { + case 'one': { + facetNames = null; + behaviorTemplate = copyMethods(behavior); + break; + } + case 'many': { + facetNames = Object.getOwnPropertyNames(behavior).sort(); assert( - innerSelf.repCount === 0, - X`${innerSelf.baseRef} already has a representative`, + facetNames.length > 1, + 'a multi-facet object must have multiple facets', ); - innerSelf.repCount += 1; + behaviorTemplate = {}; + for (const name of facetNames) { + behaviorTemplate[name] = copyMethods(behavior[name]); + } + break; } + case 'not': + assert.fail(X`invalid behavior specifier for ${q(tag)}`); + default: + assert.fail(X`unexepected facetiousness: ${q(facetiousness)}`); + } + vrm.registerKind(kindID, reanimate, deleteStoredVO, durable); + vrm.rememberFacetNames(kindID, facetNames); + harden(behaviorTemplate); + + function makeRepresentative(innerSelf, initializing) { + assert( + innerSelf.repCount === 0, + X`${innerSelf.baseRef} already has a representative`, + ); + innerSelf.repCount += 1; function ensureState() { if (innerSelf.rawState) { @@ -516,12 +604,12 @@ export function makeVirtualObjectManager( } } - const wrappedState = {}; + const state = {}; if (!initializing) { ensureState(); } for (const prop of Object.getOwnPropertyNames(innerSelf.rawState)) { - Object.defineProperty(wrappedState, prop, { + Object.defineProperty(state, prop, { get: () => { ensureState(); return unserialize(innerSelf.rawState[prop]); @@ -544,61 +632,54 @@ export function makeVirtualObjectManager( }, }); } - harden(wrappedState); + harden(state); if (initializing) { cache.remember(innerSelf); } - const self = actualize(wrappedState); let toHold; let toExpose; - const facetiousness = assessFacetiousness(self); - switch (facetiousness) { - case 'one': { - toHold = Far(tag, self); - vrm.checkOrAcquireFacetNames(kindID, null); - toExpose = toHold; - break; - } - case 'many': { - toExpose = {}; - toHold = []; - const facetNames = Object.getOwnPropertyNames(self).sort(); - assert( - facetNames.length > 1, - 'a multi-facet object must have multiple facets', - ); - vrm.checkOrAcquireFacetNames(kindID, facetNames); - for (const facetName of facetNames) { - const facet = Far(`${tag} ${facetName}`, self[facetName]); - toExpose[facetName] = facet; - toHold.push(facet); - facetToCohort.set(facet, toHold); - } - harden(toExpose); - break; + unweakable.add(state); + if (facetNames === null) { + const context = { state }; + // `context` does not need a linkToCohort because it holds the facets (which hold the cohort) + unweakable.add(context); + context.self = bindMethods(context, behaviorTemplate); + toHold = Far(tag, context.self); + linkToCohort.set(Object.getPrototypeOf(toHold), toHold); + unweakable.add(Object.getPrototypeOf(toHold)); + toExpose = toHold; + harden(context); + } else { + toExpose = {}; + toHold = []; + const facets = {}; + const context = { state, facets }; + for (const name of facetNames) { + facets[name] = bindMethods(context, behaviorTemplate[name]); + const facet = Far(`${tag} ${name}`, facets[name]); + linkToCohort.set(Object.getPrototypeOf(facet), facet); + unweakable.add(Object.getPrototypeOf(facet)); + toExpose[name] = facet; + toHold.push(facet); + linkToCohort.set(facet, toHold); } - case 'not': - assert.fail(X`invalid self actualization for ${q(tag)}`); - default: - assert.fail(X`unexepected facetiousness: ${q(facetiousness)}`); + unweakable.add(facets); + harden(context); + harden(facets); + harden(toExpose); + harden(toHold); } - if (!proForma) { - innerSelf.representative = toHold; - stateToRepresentative.set(wrappedState, toHold); - } - return [toHold, toExpose, wrappedState]; + innerSelf.representative = toHold; + linkToCohort.set(state, toHold); + return [toHold, toExpose, state]; } - function reanimate(baseRef, proForma) { + function reanimate(baseRef) { // kdebug(`vo reanimate ${baseRef}`); const innerSelf = cache.lookup(baseRef, false); - const [toHold] = makeRepresentative(innerSelf, false, proForma); - if (proForma) { - return null; - } else { - return toHold; - } + const [toHold] = makeRepresentative(innerSelf, false); + return toHold; } function deleteStoredVO(baseRef) { @@ -615,8 +696,6 @@ export function makeVirtualObjectManager( return doMoreGC; } - vrm.registerKind(kindID, reanimate, deleteStoredVO, durable); - function makeNewInstance(...args) { const baseRef = `o+${kindID}/${nextInstanceID}`; nextInstanceID += 1; @@ -635,14 +714,14 @@ export function makeVirtualObjectManager( rawState[prop] = data; } const innerSelf = { baseRef, rawState, repCount: 0 }; - const [toHold, toExpose, state] = makeRepresentative( - innerSelf, - true, - false, - ); + const [toHold, toExpose, state] = makeRepresentative(innerSelf, true); registerValue(baseRef, toHold, Array.isArray(toHold)); if (finish) { - finish(state, toExpose); + if (toHold === toExpose) { + finish({ state, self: toExpose }); + } else { + finish({ state, facets: toExpose }); + } } cache.markDirty(innerSelf); return toExpose; @@ -651,9 +730,9 @@ export function makeVirtualObjectManager( return makeNewInstance; } - function defineKind(tag, init, actualize, finish) { + function defineKind(tag, init, behavior, options) { const kindID = `${allocateExportID()}`; - return defineKindInternal(kindID, tag, init, actualize, finish, false); + return defineKindInternal(kindID, tag, init, behavior, options, false); } let kindIDID; @@ -669,12 +748,14 @@ export function makeVirtualObjectManager( vrm.registerKind(kindIDID, reanimateDurableKindID, () => null, true); } - function reanimateDurableKindID(vobjID, _proforma) { + function reanimateDurableKindID(vobjID) { const { subid: kindID } = parseVatSlot(vobjID); const raw = syscall.vatstoreGet(`vom.kind.${kindID}`); assert(raw, X`unknown kind ID ${kindID}`); const durableKindDescriptor = harden(JSON.parse(raw)); const kindHandle = Far('kind', {}); + linkToCohort.set(Object.getPrototypeOf(kindHandle), kindHandle); + unweakable.add(Object.getPrototypeOf(kindHandle)); kindDescriptors.set(kindHandle, durableKindDescriptor); return kindHandle; } @@ -685,6 +766,8 @@ export function makeVirtualObjectManager( const kindIDvref = `o+${kindIDID}/${kindID}`; const durableKindDescriptor = harden({ kindID, tag }); const kindHandle = Far('kind', {}); + linkToCohort.set(Object.getPrototypeOf(kindHandle), kindHandle); + unweakable.add(Object.getPrototypeOf(kindHandle)); kindDescriptors.set(kindHandle, durableKindDescriptor); registerValue(kindIDvref, kindHandle, false); syscall.vatstoreSet( @@ -694,7 +777,7 @@ export function makeVirtualObjectManager( return kindHandle; }; - function defineDurableKind(kindHandle, init, actualize, finish) { + function defineDurableKind(kindHandle, init, behavior, options) { const durableKindDescriptor = kindDescriptors.get(kindHandle); assert(durableKindDescriptor); const { kindID, tag } = durableKindDescriptor; @@ -702,8 +785,8 @@ export function makeVirtualObjectManager( kindID, tag, init, - actualize, - finish, + behavior, + options, true, ); definedDurableKinds.add(kindID); diff --git a/packages/SwingSet/src/liveslots/virtualReferences.js b/packages/SwingSet/src/liveslots/virtualReferences.js index 49b869aa62d..dd7bc9e2ce1 100644 --- a/packages/SwingSet/src/liveslots/virtualReferences.js +++ b/packages/SwingSet/src/liveslots/virtualReferences.js @@ -219,46 +219,17 @@ export function makeVirtualReferenceManager( } /** - * Compare two arrays (shallowly) for equality. - * - * @template T - * @param {T[]} a1 - * @param {T[]} a2 - * @returns {boolean} - */ - const arrayEquals = (a1, a2) => { - assert(Array.isArray(a1)); - assert(Array.isArray(a2)); - if (a1.length !== a2.length) { - return false; - } - return a1.every((elem, idx) => Object.is(a2[idx], elem)); - }; - - /** - * Check a list of facet names against what's already been established for a - * kind. If they don't match, it's an error. If nothing has been established - * yet, establish it now. + * Record the names of the facets of a multi-faceted virtual object. * * @param {string} kindID The kind we're talking about * @param {string[]|null} facetNames A sorted array of facet names to be - * checked or acquired, or null if the kind is unfaceted + * recorded, or null if the kind is unfaceted */ - function checkOrAcquireFacetNames(kindID, facetNames) { + function rememberFacetNames(kindID, facetNames) { const kindInfo = kindInfoTable.get(`${kindID}`); assert(kindInfo, `no kind info for ${kindID}`); - if (kindInfo.facetNames !== undefined) { - if (facetNames === null) { - assert(kindInfo.facetNames === null); - } else { - assert( - arrayEquals(facetNames, kindInfo.facetNames), - 'all virtual objects of the same kind must have the same facet names', - ); - } - } else { - kindInfo.facetNames = facetNames; - } + assert(kindInfo.facetNames === undefined); + kindInfo.facetNames = facetNames; } /** @@ -303,19 +274,17 @@ export function makeVirtualReferenceManager( * persistent storage. Used for deserializing. * * @param {string} baseRef The baseRef of the object being reanimated - * @param {boolean} proForma If true, representative creation is for formal - * use only and result will be ignored. * * @returns {Object} A representative of the object identified by `baseRef` */ - function reanimate(baseRef, proForma) { + function reanimate(baseRef) { const { id } = parseVatSlot(baseRef); const kindID = `${id}`; const kindInfo = kindInfoTable.get(kindID); assert(kindInfo, `no kind info for ${kindID}, call defineDurableKind`); const { reanimator } = kindInfo; if (reanimator) { - return reanimator(baseRef, proForma); + return reanimator(baseRef); } else { assert.fail(X`unknown kind ${kindID}`); } @@ -618,7 +587,7 @@ export function makeVirtualReferenceManager( droppedCollectionRegistry, isDurable, registerKind, - checkOrAcquireFacetNames, + rememberFacetNames, reanimate, addReachableVref, removeReachableVref, diff --git a/packages/SwingSet/test/stores/test-durabilityChecks.js b/packages/SwingSet/test/stores/test-durabilityChecks.js index 50e2320b034..b01747bfa28 100644 --- a/packages/SwingSet/test/stores/test-durabilityChecks.js +++ b/packages/SwingSet/test/stores/test-durabilityChecks.js @@ -13,17 +13,17 @@ const { defineKind, defineDurableKind, makeKindHandle } = vom; const durableHolderKind = makeKindHandle('holder'); const initHolder = (held = null) => ({ held }); -const actualizeHolder = state => ({ - hold: value => { +const holderBehavior = { + hold: ({ state }, value) => { state.held = value; }, -}); +}; -const makeVirtualHolder = defineKind('holder', initHolder, actualizeHolder); +const makeVirtualHolder = defineKind('holder', initHolder, holderBehavior); const makeDurableHolder = defineDurableKind( durableHolderKind, initHolder, - actualizeHolder, + holderBehavior, ); const aString = 'zorch!'; diff --git a/packages/SwingSet/test/test-vat-env.js b/packages/SwingSet/test/test-vat-env.js index bd325b3d2c8..0c51f94a070 100644 --- a/packages/SwingSet/test/test-vat-env.js +++ b/packages/SwingSet/test/test-vat-env.js @@ -8,17 +8,17 @@ test('harden from SES is in the test environment', t => { t.pass(); }); -const actualizeThing = _state => ({ +const thingBehavior = { ping: () => 4, -}); +}; test('kind makers are in the test environment', t => { - const makeVThing = VatData.defineKind('thing', null, actualizeThing); + const makeVThing = VatData.defineKind('thing', null, thingBehavior); const vthing = makeVThing('vthing'); t.is(vthing.ping(), 4); const kind = VatData.makeKindHandle('thing'); - const makeDThing = VatData.defineDurableKind(kind, null, actualizeThing); + const makeDThing = VatData.defineDurableKind(kind, null, thingBehavior); const dthing = makeDThing('dthing'); t.is(dthing.ping(), 4); }); diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-1.js b/packages/SwingSet/test/upgrade/vat-ulrik-1.js index 3317d911b00..52a4b6f7994 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-1.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-1.js @@ -8,20 +8,20 @@ const durandalHandle = makeKindHandle('durandal'); function initializeDurandal(arg) { return { arg }; } -function actualizeDurandal(state) { - return { - get() { - return state.arg; - }, - set(arg) { - state.arg = arg; - }, - }; -} + +const durandalBehavior = { + get({ state }) { + return state.arg; + }, + set({ state }, arg) { + state.arg = arg; + }, +}; + const makeDurandal = defineDurableKind( durandalHandle, initializeDurandal, - actualizeDurandal, + durandalBehavior, ); export function buildRootObject(_vatPowers, vatParameters, baggage) { diff --git a/packages/SwingSet/test/upgrade/vat-ulrik-2.js b/packages/SwingSet/test/upgrade/vat-ulrik-2.js index 9439197eb5e..5ac945ba235 100644 --- a/packages/SwingSet/test/upgrade/vat-ulrik-2.js +++ b/packages/SwingSet/test/upgrade/vat-ulrik-2.js @@ -6,20 +6,19 @@ const { defineDurableKind } = VatData; function initializeDurandal(arg) { return { arg }; } -function actualizeDurandal(state) { - return { - get() { - return `new ${state.arg}`; - }, - set(arg) { - state.arg = arg; - }, - }; -} + +const durandalBehavior = { + get({ state }) { + return `new ${state.arg}`; + }, + set({ state }, arg) { + state.arg = arg; + }, +}; export function buildRootObject(_vatPowers, vatParameters, baggage) { const durandalHandle = baggage.get('durandalHandle'); - defineDurableKind(durandalHandle, initializeDurandal, actualizeDurandal); + defineDurableKind(durandalHandle, initializeDurandal, durandalBehavior); return Far('root', { getVersion() { diff --git a/packages/SwingSet/test/virtualObjects/double-retire-import/vat-dri.js b/packages/SwingSet/test/virtualObjects/double-retire-import/vat-dri.js index 78086ca5a51..2abe2fb0909 100644 --- a/packages/SwingSet/test/virtualObjects/double-retire-import/vat-dri.js +++ b/packages/SwingSet/test/virtualObjects/double-retire-import/vat-dri.js @@ -6,17 +6,15 @@ const { defineKind } = VatData; function initialize(arg) { return harden({ arg }); } -function actualize(state) { - return { - get() { - return state.arg; - }, - set(arg) { - state.arg = harden(arg); - }, - }; -} -const makeVir = defineKind('virtual', initialize, actualize); +const behavior = { + get({ state }) { + return state.arg; + }, + set({ state }, arg) { + state.arg = harden(arg); + }, +}; +const makeVir = defineKind('virtual', initialize, behavior); function buildVirtuals(sensor0, sensor1) { // eslint-disable-next-line no-unused-vars diff --git a/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js b/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js index 2ef75be6cb0..a7621589efa 100644 --- a/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js +++ b/packages/SwingSet/test/virtualObjects/test-reachable-vrefs.js @@ -14,21 +14,13 @@ test('VOM tracks reachable vrefs', async t => { const weakStore = makeScalarBigWeakMapStore('test'); // empty object, used as weap map store key - const makeKey = defineKind( - 'key', - () => ({}), - _state => ({}), - ); - const makeHolder = defineKind( - 'holder', - held => ({ held }), - state => ({ - setHeld: held => { - state.held = held; - }, - getHeld: () => state.held, - }), - ); + const makeKey = defineKind('key', () => ({}), {}); + const makeHolder = defineKind('holder', held => ({ held }), { + setHeld: ({ state }, held) => { + state.held = held; + }, + getHeld: ({ state }) => state.held, + }); let count = 1001; function makePresence() { diff --git a/packages/SwingSet/test/virtualObjects/test-representatives.js b/packages/SwingSet/test/virtualObjects/test-representatives.js index ed02b8a4c84..b60093345bb 100644 --- a/packages/SwingSet/test/virtualObjects/test-representatives.js +++ b/packages/SwingSet/test/virtualObjects/test-representatives.js @@ -401,7 +401,7 @@ test('virtual object gc', async t => { }); // Check that facets which don't reference their state still kill their cohort alive -test('empty facets are not orphaned', async t => { +async function orphanTest(t, mode) { const config = { bootstrap: 'bootstrap', vats: { @@ -420,7 +420,7 @@ test('empty facets are not orphaned', async t => { const hostStorage = provideHostStorage(); - const c = await buildVatController(config, [], { hostStorage }); + const c = await buildVatController(config, [mode], { hostStorage }); c.pinVatRoot('bootstrap'); await c.run(); @@ -428,5 +428,53 @@ test('empty facets are not orphaned', async t => { c.kpResolution(c.bootstrapResult), capargs({ '@qclass': 'undefined' }), ); - t.deepEqual(c.dump().log, ['compare originalFacet === thing : true']); + t.deepEqual(c.dump().log, ['compare old === new : true']); +} + +test('strongly held facet retains representative', async t => { + await orphanTest(t, 'facet'); +}); + +test('weakly held facet retains representative', async t => { + await orphanTest(t, 'wfacet'); +}); + +test('strongly held empty facet retains representative', async t => { + await orphanTest(t, 'empty'); +}); + +test('weakly held empty facet retains representative', async t => { + await orphanTest(t, 'wempty'); +}); + +test('strongly held method retains representative', async t => { + await orphanTest(t, 'method'); +}); + +test('weakly held method retains representative', async t => { + await orphanTest(t, 'wmethod'); +}); + +test('strongly held proto retains representative', async t => { + await orphanTest(t, 'proto'); +}); + +test('weakly held proto retains representative', async t => { + await orphanTest(t, 'wproto'); +}); + +test('strongly held cohort retains representative', async t => { + await orphanTest(t, 'cohort'); +}); + +test('weakly held cohort retains representative', async t => { + await orphanTest(t, 'wcohort'); +}); + +test('strongly held state retains representative', async t => { + await orphanTest(t, 'state'); +}); + +test('weakly held state retains representative', async t => { + await orphanTest(t, 'wstate'); }); diff --git a/packages/SwingSet/test/virtualObjects/test-retain-remotable.js b/packages/SwingSet/test/virtualObjects/test-retain-remotable.js index 76b0dadc998..c4715931263 100644 --- a/packages/SwingSet/test/virtualObjects/test-retain-remotable.js +++ b/packages/SwingSet/test/virtualObjects/test-retain-remotable.js @@ -60,21 +60,13 @@ test('remotables retained by virtualized data', async t => { const { makeScalarBigWeakMapStore } = cm; const weakStore = makeScalarBigWeakMapStore('ws'); // empty object, used as weak map store key - const makeKey = defineKind( - 'key', - () => ({}), - _state => ({}), - ); - const makeHolder = defineKind( - 'holder', - held => ({ held }), - state => ({ - setHeld: held => { - state.held = held; - }, - getHeld: () => state.held, - }), - ); + const makeKey = defineKind('key', () => ({}), {}); + const makeHolder = defineKind('holder', held => ({ held }), { + setHeld: ({ state }, held) => { + state.held = held; + }, + getHeld: ({ state }) => state.held, + }); // create a Remotable and assign it a vref, then drop it, to make sure the // fake VOM isn't holding onto a strong reference, which would cause a diff --git a/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js b/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js index b1e0b03f32e..f55793549a5 100644 --- a/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js +++ b/packages/SwingSet/test/virtualObjects/test-virtualObjectGC.js @@ -192,25 +192,17 @@ function buildRootObject(vatPowers) { const { defineKind } = VatData; - const makeThing = defineKind( - 'thing', - label => ({ label }), - state => ({ - getLabel: () => state.label, - }), - ); - const makeFacetedThing = defineKind( - 'thing', - label => ({ label }), - state => ({ - facetA: { - getLabelA: () => state.label, - }, - facetB: { - getLabelB: () => state.label, - }, - }), - ); + const makeThing = defineKind('thing', label => ({ label }), { + getLabel: ({ state }) => state.label, + }); + const makeFacetedThing = defineKind('thing', label => ({ label }), { + facetA: { + getLabelA: ({ state }) => state.label, + }, + facetB: { + getLabelB: ({ state }) => state.label, + }, + }); const cacheDisplacer = makeThing('cacheDisplacer'); // This immediately goes out of scope and gets GC'd and deleted, but its // creation consumes the same subID in its kind as the `cacheDisplacer` that @@ -220,29 +212,25 @@ function buildRootObject(vatPowers) { // eslint-disable-next-line no-unused-vars const unusedFacetedCacheDisplacer = makeFacetedThing('cacheDisplacer'); - const makeVirtualHolder = defineKind( - 'holder', - (held = null) => ({ held }), - state => ({ - setValue: value => { - state.held = value; - }, - getValue: () => state.held, - }), - ); + const makeVirtualHolder = defineKind('holder', (held = null) => ({ held }), { + setValue: ({ state }, value) => { + state.held = value; + }, + getValue: ({ state }) => state.held, + }); const virtualHolder = makeVirtualHolder(); const makeDualMarkerThing = defineKind( 'thing', () => ({ unused: 'uncared for' }), - _state => ({ + { facetA: { methodA: () => 0, }, facetB: { methodB: () => 0, }, - }), + }, ); let nextThingNumber = 0; diff --git a/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js b/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js index 6151868bf40..d3f9af2eac6 100644 --- a/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js +++ b/packages/SwingSet/test/virtualObjects/test-virtualObjectManager.js @@ -12,28 +12,26 @@ function capdata(body, slots = []) { function initThing(label = 'thing', counter = 0) { return { counter, label, resetCounter: 0 }; } -function actualizeThing(state) { - return { - inc() { - state.counter += 1; - return state.counter; - }, - reset(newStart) { - state.counter = newStart; - state.resetCounter += 1; - return state.resetCounter; - }, - relabel(newLabel) { - state.label = newLabel; - }, - get() { - return state.counter; - }, - describe() { - return `${state.label} counter has been reset ${state.resetCounter} times and is now ${state.counter}`; - }, - }; -} +const thingBehavior = { + inc({ state }) { + state.counter += 1; + return state.counter; + }, + reset({ state }, newStart) { + state.counter = newStart; + state.resetCounter += 1; + return state.resetCounter; + }, + relabel({ state }, newLabel) { + state.label = newLabel; + }, + get({ state }) { + return state.counter; + }, + describe({ state }) { + return `${state.label} counter has been reset ${state.resetCounter} times and is now ${state.counter}`; + }, +}; function thingVal(counter, label, resetCounter) { return JSON.stringify({ @@ -57,23 +55,21 @@ function minThing(label) { function initZot(arbitrary = 47, name = 'Bob', tag = 'say what?') { return { arbitrary, name, tag, count: 0 }; } -function actualizeZot(state) { - return { - sayHello(msg) { - state.count += 1; - return `${msg} ${state.name}`; - }, - rename(newName) { - state.name = newName; - state.count += 1; - return state.name; - }, - getInfo() { - state.count += 1; - return `zot ${state.name} tag=${state.tag} count=${state.count} arbitrary=${state.arbitrary}`; - }, - }; -} +const zotBehavior = { + sayHello({ state }, msg) { + state.count += 1; + return `${msg} ${state.name}`; + }, + rename({ state }, newName) { + state.name = newName; + state.count += 1; + return state.name; + }, + getInfo({ state }) { + state.count += 1; + return `zot ${state.name} tag=${state.tag} count=${state.count} arbitrary=${state.arbitrary}`; + }, +}; function zotVal(arbitrary, name, tag, count) { return JSON.stringify({ @@ -88,31 +84,29 @@ test('multifaceted virtual objects', t => { const log = []; const { defineKind } = makeFakeVirtualObjectManager({ cacheSize: 0, log }); + const getName = ({ state }) => state.name; + const getCount = ({ state }) => state.count; const makeMultiThing = defineKind( 'multithing', name => ({ name, count: 0, }), - state => { - const getName = () => state.name; - const getCount = () => state.count; - return { - incr: { - inc: () => { - state.count += 1; - }, - getName, - getCount, + { + incr: { + inc: ({ state }) => { + state.count += 1; }, - decr: { - dec: () => { - state.count -= 1; - }, - getName, - getCount, + getName, + getCount, + }, + decr: { + dec: ({ state }) => { + state.count -= 1; }, - }; + getName, + getCount, + }, }, ); const kid = 'o+2'; @@ -149,9 +143,9 @@ test('virtual object operations', t => { const log = []; const { defineKind, flushCache, dumpStore } = makeFakeVirtualObjectManager({ cacheSize: 3, log }); - const makeThing = defineKind('thing', initThing, actualizeThing); + const makeThing = defineKind('thing', initThing, thingBehavior); const tid = 'o+2'; - const makeZot = defineKind('zot', initZot, actualizeZot); + const makeZot = defineKind('zot', initZot, zotBehavior); const zid = 'o+3'; // phase 0: start @@ -375,10 +369,10 @@ test('virtual object cycles using the finish function', t => { const makeOtherThing = defineKind( 'otherThing', (name, firstThing) => ({ name, firstThing }), - state => ({ - getName: () => state.name, - getFirstThing: () => state.firstThing, - }), + { + getName: ({ state }) => state.name, + getFirstThing: ({ state }) => state.firstThing, + }, ); const makeFirstThing = defineKind( 'firstThing', @@ -386,12 +380,14 @@ test('virtual object cycles using the finish function', t => { name, otherThing: undefined, }), - state => ({ - getName: () => state.name, - getOtherThing: () => state.otherThing, - }), - (state, self) => { - state.otherThing = makeOtherThing(`${state.name}'s other thing`, self); + { + getName: ({ state }) => state.name, + getOtherThing: ({ state }) => state.otherThing, + }, + { + finish: ({ state, self }) => { + state.otherThing = makeOtherThing(`${state.name}'s other thing`, self); + }, }, ); @@ -451,7 +447,7 @@ test('durable kind IDs can be reanimated', t => { t.deepEqual(log, []); // Use it now, to define a durable kind - const makeThing = defineDurableKind(fetchedKindID, initThing, actualizeThing); + const makeThing = defineDurableKind(fetchedKindID, initThing, thingBehavior); t.deepEqual(log, []); // Make an instance of the new kind, just to be sure it's there @@ -471,17 +467,13 @@ test('virtual object gc', t => { const { setExportStatus, possibleVirtualObjectDeath } = vrm; const { deleteEntry, dumpStore } = fakeStuff; - const makeThing = defineKind('thing', initThing, actualizeThing); + const makeThing = defineKind('thing', initThing, thingBehavior); const tbase = 'o+10'; - const makeRef = defineKind( - 'ref', - value => ({ value }), - state => ({ - setVal: value => { - state.value = value; - }, - }), - ); + const makeRef = defineKind('ref', value => ({ value }), { + setVal: ({ state }, value) => { + state.value = value; + }, + }); t.is(log.shift(), `get kindIDID => undefined`); t.is(log.shift(), `set kindIDID 1`); @@ -738,8 +730,8 @@ test('weak store operations', t => { const { defineKind } = vom; const { makeScalarBigWeakMapStore } = cm; - const makeThing = defineKind('thing', initThing, actualizeThing); - const makeZot = defineKind('zot', initZot, actualizeZot); + const makeThing = defineKind('thing', initThing, thingBehavior); + const makeZot = defineKind('zot', initZot, zotBehavior); const thing1 = makeThing('t1'); const thing2 = makeThing('t2'); @@ -783,8 +775,8 @@ test('virtualized weak collection operations', t => { const { VirtualObjectAwareWeakMap, VirtualObjectAwareWeakSet, defineKind } = makeFakeVirtualObjectManager({ cacheSize: 3 }); - const makeThing = defineKind('thing', initThing, actualizeThing); - const makeZot = defineKind('zot', initZot, actualizeZot); + const makeThing = defineKind('thing', initThing, thingBehavior); + const makeZot = defineKind('zot', initZot, zotBehavior); const thing1 = makeThing('t1'); const thing2 = makeThing('t2'); diff --git a/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js b/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js index dc0aaa97772..84ce6b21c78 100644 --- a/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js +++ b/packages/SwingSet/test/virtualObjects/vat-orphan-bob.js @@ -4,29 +4,126 @@ import { defineKind } from '@agoric/vat-data'; export function buildRootObject(vatPowers) { const { testLog } = vatPowers; - const makeThing = defineKind( - 'thing', - () => ({ unused: 'uncared for' }), - () => ({ - facetA: { - methodA: () => 0, + let extracted; + + const makeThing = defineKind('thing', () => ({}), { + regularFacet: { + statelessMethod: () => 0, + extractState: ({ state }) => { + extracted = state; }, - facetB: { - methodB: () => 0, + extractCohort: ({ facets }) => { + extracted = facets; }, - }), - ); + }, + emptyFacet: {}, + }); - let originalFacet; + let strongRetainer; + const weakRetainer = new WeakSet(); + let retentionMode; return Far('root', { - getYourThing() { - originalFacet = makeThing().facetA; - makeThing(); + retain(mode) { + retentionMode = mode; + const { regularFacet, emptyFacet } = makeThing(); + const originalFacet = mode.endsWith('empty') ? emptyFacet : regularFacet; + switch (mode) { + case 'facet': + case 'empty': + strongRetainer = originalFacet; + break; + case 'wfacet': + case 'wempty': + weakRetainer.add(originalFacet); + break; + case 'method': + strongRetainer = originalFacet.statelessMethod; + break; + case 'wmethod': + weakRetainer.add(originalFacet.statelessMethod); + break; + case 'proto': + // eslint-disable-next-line no-proto + strongRetainer = originalFacet.__proto__; + break; + case 'wproto': + // eslint-disable-next-line no-proto + weakRetainer.add(originalFacet.__proto__); + break; + case 'cohort': + originalFacet.extractCohort(); + strongRetainer = extracted; + extracted = null; + break; + case 'wcohort': + originalFacet.extractCohort(); + weakRetainer.add(extracted); + extracted = null; + break; + case 'state': + originalFacet.extractState(); + strongRetainer = extracted; + extracted = null; + break; + case 'wstate': + originalFacet.extractState(); + weakRetainer.add(extracted); + extracted = null; + break; + default: + console.log(`retain: unknown mode ${mode}`); + break; + } + makeThing(); // push original out of the cache return originalFacet; }, - isThingYourThing(thing) { - testLog(`compare originalFacet === thing : ${originalFacet === thing}`); + testForRetention(facet) { + let compare; + switch (retentionMode) { + case 'facet': + case 'empty': + compare = strongRetainer === facet; + break; + case 'wfacet': + case 'wempty': + compare = weakRetainer.has(facet); + break; + case 'method': + compare = strongRetainer === facet.statelessMethod; + break; + case 'wmethod': + compare = weakRetainer.has(facet.statelessMethod); + break; + case 'proto': + // eslint-disable-next-line no-proto + compare = strongRetainer === facet.__proto__; + break; + case 'wproto': + // eslint-disable-next-line no-proto + compare = weakRetainer.has(facet.__proto__); + break; + case 'cohort': + facet.extractCohort(); + compare = strongRetainer === extracted; + break; + case 'wcohort': + facet.extractCohort(); + compare = weakRetainer.has(extracted); + break; + case 'state': + facet.extractState(); + compare = strongRetainer === extracted; + break; + case 'wstate': + facet.extractState(); + compare = weakRetainer.has(extracted); + break; + default: + console.log(`testForRetention: unknown mode ${retentionMode}`); + break; + } + testLog(`compare old === new : ${compare}`); }, }); } diff --git a/packages/SwingSet/test/virtualObjects/vat-orphan-bootstrap.js b/packages/SwingSet/test/virtualObjects/vat-orphan-bootstrap.js index 748281f62cd..f46a496cac0 100644 --- a/packages/SwingSet/test/virtualObjects/vat-orphan-bootstrap.js +++ b/packages/SwingSet/test/virtualObjects/vat-orphan-bootstrap.js @@ -1,11 +1,12 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -export function buildRootObject(_vatPowers) { +export function buildRootObject(_vatPowers, vatParameters) { return Far('root', { async bootstrap(vats) { - const thing = await E(vats.bob).getYourThing(); - await E(vats.bob).isThingYourThing(thing); + const mode = vatParameters.argv[0]; + const thing = await E(vats.bob).retain(mode); + await E(vats.bob).testForRetention(thing); }, }); } diff --git a/packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js b/packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js index eaa4d0f7ecf..b4cedb6ca66 100644 --- a/packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js +++ b/packages/SwingSet/test/virtualObjects/vat-representative-bootstrap.js @@ -6,12 +6,12 @@ const makeThing = defineKind( name => { return { name }; }, - state => ({ - getName: () => state.name, - rename: newName => { + { + getName: ({ state }) => state.name, + rename: ({ state }, newName) => { state.name = newName; }, - }), + }, ); export function buildRootObject() { diff --git a/packages/SwingSet/test/virtualObjects/vat-vom-gc-bob.js b/packages/SwingSet/test/virtualObjects/vat-vom-gc-bob.js index 331018831b0..5c8831dad59 100644 --- a/packages/SwingSet/test/virtualObjects/vat-vom-gc-bob.js +++ b/packages/SwingSet/test/virtualObjects/vat-vom-gc-bob.js @@ -5,13 +5,9 @@ import { defineKind } from '@agoric/vat-data'; const things = []; export function buildRootObject(_vatPowers) { - const makeThing = defineKind( - 'thing', - label => ({ label }), - state => ({ - getLabel: () => state.label, - }), - ); + const makeThing = defineKind('thing', label => ({ label }), { + getLabel: ({ state }) => state.label, + }); let nextThingNumber = 0; return Far('root', { diff --git a/packages/SwingSet/test/virtualObjects/vat-weakcollections-alice.js b/packages/SwingSet/test/virtualObjects/vat-weakcollections-alice.js index 0a983f31562..aa138243d43 100644 --- a/packages/SwingSet/test/virtualObjects/vat-weakcollections-alice.js +++ b/packages/SwingSet/test/virtualObjects/vat-weakcollections-alice.js @@ -1,16 +1,12 @@ import { Far } from '@endo/marshal'; import { defineKind } from '@agoric/vat-data'; -const makeHolder = defineKind( - 'holder-vo', - value => ({ value }), - state => ({ - getValue: () => state.value, - setValue: newValue => { - state.value = newValue; - }, - }), -); +const makeHolder = defineKind('holder-vo', value => ({ value }), { + getValue: ({ state }) => state.value, + setValue: ({ state }, newValue) => { + state.value = newValue; + }, +}); export function buildRootObject() { const testWeakMap = new WeakMap(); diff --git a/packages/SwingSet/tools/fakeVirtualSupport.js b/packages/SwingSet/tools/fakeVirtualSupport.js index f5efb28f0a0..129aadd1979 100644 --- a/packages/SwingSet/tools/fakeVirtualSupport.js +++ b/packages/SwingSet/tools/fakeVirtualSupport.js @@ -182,14 +182,11 @@ export function makeFakeLiveSlotsStuff(options = {}) { assert.equal(type, 'object'); let val = getValForSlot(slot); if (val) { - if (virtual) { - vrm.reanimate(slot, true); - } return val; } if (virtual) { if (vrm) { - val = vrm.reanimate(slot, false); + val = vrm.reanimate(slot); } else { assert.fail('fake liveSlots stuff configured without vrm'); } diff --git a/packages/run-protocol/src/vaultFactory/vault.js b/packages/run-protocol/src/vaultFactory/vault.js index d5b04f4cb0b..665c8fe65c2 100644 --- a/packages/run-protocol/src/vaultFactory/vault.js +++ b/packages/run-protocol/src/vaultFactory/vault.js @@ -106,6 +106,13 @@ const validTransitions = { * }} MutableState */ +/** + * @typedef {{ + * state: ImmutableState & MutableState, + * facets: *, + * }} MethodContext + */ + /** * @param {ZCF} zcf * @param {InnerVaultManagerBase & GetVaultParams} manager @@ -135,24 +142,58 @@ const initState = (zcf, manager, idInManager) => { }); }; -/** @param {ImmutableState & MutableState} state */ -const constructFromState = state => { - /** @type {ImmutableState} */ - const { idInManager, manager, zcf } = state; +/** + * Check whether we can proceed with an `adjustBalances`. + * + * @param {Amount} newCollateralPre + * @param {Amount} maxDebtPre + * @param {Amount} newCollateral + * @param {Amount} newDebt + * @returns {boolean} + */ +const checkRestart = (newCollateralPre, maxDebtPre, newCollateral, newDebt) => { + if (AmountMath.isGTE(newCollateralPre, newCollateral)) { + // The collateral did not go up. If the collateral decreased, we pro-rate maxDebt. + // We can pro-rate maxDebt because the quote is either linear (price is + // unchanging) or super-linear (also called "convex"). Super-linear is from + // AMMs: selling less collateral would mean an even smaller price impact, so + // this is a conservative choice. + const debtPerCollateral = makeRatioFromAmounts( + maxDebtPre, + newCollateralPre, + ); + // `floorMultiply` because the debt ceiling should be tight + const maxDebtAfter = floorMultiplyBy(newCollateral, debtPerCollateral); + assert( + AmountMath.isGTE(maxDebtAfter, newDebt), + X`The requested debt ${q( + newDebt, + )} is more than the collateralization ratio allows: ${q(maxDebtAfter)}`, + ); + // The `collateralAfter` can still cover the `newDebt`, so don't restart. + return false; + } + // The collateral went up. Restart if the debt *also* went up because + // the price quote might not apply at the higher numbers. + return !AmountMath.isGTE(maxDebtPre, newDebt); +}; +const helperBehavior = { // #region Computed constants - const collateralBrand = manager.getCollateralBrand(); - const debtBrand = manager.getDebtBrand(); + collateralBrand: ({ state }) => state.manager.getCollateralBrand(), + debtBrand: ({ state }) => state.manager.getDebtBrand(), - const emptyCollateral = AmountMath.makeEmpty(collateralBrand); - const emptyDebt = AmountMath.makeEmpty(debtBrand); + emptyCollateral: ({ facets }) => + AmountMath.makeEmpty(facets.helper.collateralBrand()), + emptyDebt: ({ facets }) => AmountMath.makeEmpty(facets.helper.debtBrand()), // #endregion // #region Phase logic /** + * @param {MethodContext} context * @param {InnerPhase} newPhase */ - const assignPhase = newPhase => { + assignPhase: ({ state }, newPhase) => { const { phase } = state; const validNewPhases = validTransitions[phase]; assert( @@ -160,145 +201,115 @@ const constructFromState = state => { `Vault cannot transition from ${phase} to ${newPhase}`, ); state.phase = newPhase; - }; + }, - const assertActive = () => { + assertActive: ({ state }) => { const { phase } = state; assert(phase === VaultPhase.ACTIVE); - }; + }, - const assertCloseable = () => { + assertCloseable: ({ state }) => { const { phase } = state; assert( phase === VaultPhase.ACTIVE || phase === VaultPhase.LIQUIDATED, X`to be closed a vault must be active or liquidated, not ${phase}`, ); - }; + }, // #endregion /** * Called whenever the debt is paid or created through a transaction, * but not for interest accrual. * + * @param {MethodContext} context * @param {Amount} newDebt - principal and all accrued interest */ - const updateDebtSnapshot = newDebt => { + updateDebtSnapshot: ({ state }, newDebt) => { // update local state state.debtSnapshot = newDebt; - state.interestSnapshot = manager.getCompoundedInterest(); - }; + state.interestSnapshot = state.manager.getCompoundedInterest(); + }, /** * Update the debt balance and propagate upwards to * maintain aggregate debt and liquidation order. * + * @param {MethodContext} context * @param {Amount} oldDebt - prior principal and all accrued interest * @param {Amount} oldCollateral - actual collateral * @param {Amount} newDebt - actual principal and all accrued interest */ - const updateDebtAccounting = (oldDebt, oldCollateral, newDebt) => { - updateDebtSnapshot(newDebt); + updateDebtAccounting: ( + { state, facets }, + oldDebt, + oldCollateral, + newDebt, + ) => { + const { helper } = facets; + helper.updateDebtSnapshot(newDebt); // update position of this vault in liquidation priority queue - manager.updateVaultPriority(oldDebt, oldCollateral, idInManager); - }; - - /** - * The actual current debt, including accrued interest. - * - * This looks like a simple getter but it does a lot of the heavy lifting for - * interest accrual. Rather than updating all records when interest accrues, - * the vault manager updates just its rolling compounded interest. Here we - * calculate what the current debt is given what's recorded in this vault and - * what interest has compounded since this vault record was written. - * - * @see getNormalizedDebt - * @returns {Amount<'nat'>} - */ - const getCurrentDebt = () => { - return calculateCurrentDebt( - state.debtSnapshot, - state.interestSnapshot, - manager.getCompoundedInterest(), + state.manager.updateVaultPriority( + oldDebt, + oldCollateral, + state.idInManager, ); - }; - - /** - * The normalization puts all debts on a common time-independent scale since - * the launch of this vault manager. This allows the manager to order vaults - * by their debt-to-collateral ratios without having to mutate the debts as - * the interest accrues. - * - * @see getActualDebAmount - * @returns {Amount<'nat'>} as if the vault was open at the launch of this manager, before any interest accrued - */ - const getNormalizedDebt = () => { - return reverseInterest(state.debtSnapshot, state.interestSnapshot); - }; + }, - const getCollateralAllocated = seat => - seat.getAmountAllocated('Collateral', collateralBrand); - const getRunAllocated = seat => seat.getAmountAllocated('RUN', debtBrand); + getCollateralAllocated: ({ facets }, seat) => + seat.getAmountAllocated('Collateral', facets.helper.collateralBrand()), + getRunAllocated: ({ facets }, seat) => + seat.getAmountAllocated('RUN', facets.helper.debtBrand()), - const assertVaultHoldsNoRun = () => { + assertVaultHoldsNoRun: ({ state, facets }) => { const { vaultSeat } = state; assert( - AmountMath.isEmpty(getRunAllocated(vaultSeat)), + AmountMath.isEmpty(facets.helper.getRunAllocated(vaultSeat)), X`Vault should be empty of RUN`, ); - }; + }, - const assertSufficientCollateral = async ( + assertSufficientCollateral: async ( + { state, facets }, collateralAmount, proposedRunDebt, ) => { - const maxRun = await manager.maxDebtFor(collateralAmount); + const maxRun = await state.manager.maxDebtFor(collateralAmount); assert( - AmountMath.isGTE(maxRun, proposedRunDebt, debtBrand), + AmountMath.isGTE(maxRun, proposedRunDebt, facets.helper.debtBrand()), X`Requested ${q(proposedRunDebt)} exceeds max ${q(maxRun)}`, ); - }; - - /** - * - * @returns {Amount<'nat'>} - */ - const getCollateralAmount = () => { - const { vaultSeat } = state; - // getCollateralAllocated would return final allocations - return vaultSeat.hasExited() - ? emptyCollateral - : getCollateralAllocated(vaultSeat); - }; + }, /** * + * @param {MethodContext} context * @param {OuterPhase} newPhase */ - const snapshotState = newPhase => { + snapshotState: ({ state, facets }, newPhase) => { const { debtSnapshot: debt, interestSnapshot: interest } = state; /** @type {VaultUIState} */ return harden({ // TODO move manager state to a separate notifer https://github.com/Agoric/agoric-sdk/issues/4540 - interestRate: manager.getInterestRate(), - liquidationRatio: manager.getLiquidationMargin(), + interestRate: state.manager.getInterestRate(), + liquidationRatio: state.manager.getLiquidationMargin(), debtSnapshot: { debt, interest }, - locked: getCollateralAmount(), + locked: facets.self.getCollateralAmount(), // newPhase param is so that makeTransferInvitation can finish without setting the vault's phase // TODO refactor https://github.com/Agoric/agoric-sdk/issues/4415 vaultState: newPhase, }); - }; + }, // call this whenever anything changes! - const updateUiState = () => { + updateUiState: ({ state, facets }) => { const { outerUpdater } = state; if (!outerUpdater) { console.warn('updateUiState called after outerUpdater removed'); return; } const { phase } = state; - const uiState = snapshotState(phase); - trace('updateUiState', idInManager, uiState); + const uiState = facets.helper.snapshotState(phase); + trace('updateUiState', state.idInManager, uiState); switch (phase) { case VaultPhase.ACTIVE: @@ -313,28 +324,12 @@ const constructFromState = state => { default: throw Error(`unreachable vault phase: ${phase}`); } - }; - - /** - * Call must check for and remember shortfall - * - * @param {Amount} newDebt - */ - const liquidated = newDebt => { - updateDebtSnapshot(newDebt); - - assignPhase(VaultPhase.LIQUIDATED); - updateUiState(); - }; - - const liquidating = () => { - assignPhase(VaultPhase.LIQUIDATING); - updateUiState(); - }; + }, - /** @type {OfferHandler} */ - const closeHook = async seat => { - assertCloseable(); + /* * @type {OfferHandler} XXX needs a proper type def; this one won't do */ + closeHook: async ({ state, facets }, seat) => { + const { self, helper } = facets; + helper.assertCloseable(); const { phase, vaultSeat } = state; if (phase === VaultPhase.ACTIVE) { assertProposalShape(seat, { @@ -342,7 +337,7 @@ const constructFromState = state => { }); // you're paying off the debt, you get everything back. - const debt = getCurrentDebt(); + const debt = self.getCurrentDebt(); const { give: { RUN: given }, } = seat.getProposal(); @@ -356,33 +351,28 @@ const constructFromState = state => { // Return any overpayment seat.incrementBy(vaultSeat.decrementBy(vaultSeat.getCurrentAllocation())); - zcf.reallocate(seat, vaultSeat); - manager.burnAndRecord(debt, seat); + state.zcf.reallocate(seat, vaultSeat); + state.manager.burnAndRecord(debt, seat); } else if (phase === VaultPhase.LIQUIDATED) { // Simply reallocate vault assets to the offer seat. // Don't take anything from the offer, even if vault is underwater. // TODO verify that returning RUN here doesn't mess up debt limits seat.incrementBy(vaultSeat.decrementBy(vaultSeat.getCurrentAllocation())); - zcf.reallocate(seat, vaultSeat); + state.zcf.reallocate(seat, vaultSeat); } else { throw new Error('only active and liquidated vaults can be closed'); } seat.exit(); - assignPhase(VaultPhase.CLOSED); - updateDebtSnapshot(emptyDebt); - updateUiState(); + helper.assignPhase(VaultPhase.CLOSED); + helper.updateDebtSnapshot(helper.emptyDebt()); + helper.updateUiState(); - assertVaultHoldsNoRun(); + helper.assertVaultHoldsNoRun(); vaultSeat.exit(); return 'your loan is closed, thank you for your business'; - }; - - const makeCloseInvitation = () => { - assertCloseable(); - return zcf.makeInvitation(closeHook, 'CloseVault'); - }; + }, /** * Calculate the fee, the amount to mint and the resulting debt. @@ -391,98 +381,64 @@ const constructFromState = state => { * proposal. If the `want` is zero, the `fee` will also be zero, * so the simple math works. * + * @param {MethodContext} context * @param {Amount} currentDebt * @param {Amount} giveAmount * @param {Amount} wantAmount */ - const loanFee = (currentDebt, giveAmount, wantAmount) => { - const fee = ceilMultiplyBy(wantAmount, manager.getLoanFee()); + loanFee: ({ state }, currentDebt, giveAmount, wantAmount) => { + const fee = ceilMultiplyBy(wantAmount, state.manager.getLoanFee()); const toMint = AmountMath.add(wantAmount, fee); const newDebt = addSubtract(currentDebt, toMint, giveAmount); return { newDebt, toMint, fee }; - }; - - /** - * Check whether we can proceed with an `adjustBalances`. - * - * @param {Amount} newCollateralPre - * @param {Amount} maxDebtPre - * @param {Amount} newCollateral - * @param {Amount} newDebt - * @returns {boolean} - */ - const checkRestart = ( - newCollateralPre, - maxDebtPre, - newCollateral, - newDebt, - ) => { - if (AmountMath.isGTE(newCollateralPre, newCollateral)) { - // The collateral did not go up. If the collateral decreased, we pro-rate maxDebt. - // We can pro-rate maxDebt because the quote is either linear (price is - // unchanging) or super-linear (also called "convex"). Super-linear is from - // AMMs: selling less collateral would mean an even smaller price impact, so - // this is a conservative choice. - const debtPerCollateral = makeRatioFromAmounts( - maxDebtPre, - newCollateralPre, - ); - // `floorMultiply` because the debt ceiling should be tight - const maxDebtAfter = floorMultiplyBy(newCollateral, debtPerCollateral); - assert( - AmountMath.isGTE(maxDebtAfter, newDebt), - X`The requested debt ${q( - newDebt, - )} is more than the collateralization ratio allows: ${q(maxDebtAfter)}`, - ); - // The `collateralAfter` can still cover the `newDebt`, so don't restart. - return false; - } - // The collateral went up. Restart if the debt *also* went up because - // the price quote might not apply at the higher numbers. - return !AmountMath.isGTE(maxDebtPre, newDebt); - }; + }, /** * Adjust principal and collateral (atomically for offer safety) * + * @param {MethodContext} context * @param {ZCFSeat} clientSeat + * @returns {Promise} success message */ - const adjustBalancesHook = async clientSeat => { + adjustBalancesHook: async ({ state, facets }, clientSeat) => { + const { self, helper } = facets; const { vaultSeat, outerUpdater: updaterPre } = state; const proposal = clientSeat.getProposal(); assertOnlyKeys(proposal, ['Collateral', 'RUN']); - const debtPre = getCurrentDebt(); - const collateralPre = getCollateralAllocated(vaultSeat); + const debtPre = self.getCurrentDebt(); + const collateralPre = helper.getCollateralAllocated(vaultSeat); - const giveColl = proposal.give.Collateral || emptyCollateral; - const wantColl = proposal.want.Collateral || emptyCollateral; + const giveColl = proposal.give.Collateral || helper.emptyCollateral(); + const wantColl = proposal.want.Collateral || helper.emptyCollateral(); const newCollateralPre = addSubtract(collateralPre, giveColl, wantColl); // max debt supported by current Collateral as modified by proposal - const maxDebtPre = await manager.maxDebtFor(newCollateralPre); + const maxDebtPre = await state.manager.maxDebtFor(newCollateralPre); assert( updaterPre === state.outerUpdater, X`Transfer during vault adjustment`, ); - assertActive(); + helper.assertActive(); // After the `await`, we retrieve the vault's allocations again, // so we can compare to the debt limit based on the new values. - const collateral = getCollateralAllocated(vaultSeat); + const collateral = helper.getCollateralAllocated(vaultSeat); const newCollateral = addSubtract(collateral, giveColl, wantColl); - const debt = getCurrentDebt(); - const giveRUN = AmountMath.min(proposal.give.RUN || emptyDebt, debt); - const wantRUN = proposal.want.RUN || emptyDebt; + const debt = self.getCurrentDebt(); + const giveRUN = AmountMath.min( + proposal.give.RUN || helper.emptyDebt(), + debt, + ); + const wantRUN = proposal.want.RUN || helper.emptyDebt(); // Calculate the fee, the amount to mint and the resulting debt. We'll // verify that the target debt doesn't violate the collateralization ratio, // then mint, reallocate, and burn. - const { newDebt, fee, toMint } = loanFee(debt, giveRUN, wantRUN); + const { newDebt, fee, toMint } = helper.loanFee(debt, giveRUN, wantRUN); - trace('adjustBalancesHook', idInManager, { + trace('adjustBalancesHook', state.idInManager, { newCollateralPre, newCollateral, fee, @@ -491,42 +447,60 @@ const constructFromState = state => { }); if (checkRestart(newCollateralPre, maxDebtPre, newCollateral, newDebt)) { - return adjustBalancesHook(clientSeat); + return helper.adjustBalancesHook(clientSeat); } stageDelta(clientSeat, vaultSeat, giveColl, wantColl, 'Collateral'); // `wantRUN` is allocated in the reallocate and mint operation, and so not here - stageDelta(clientSeat, vaultSeat, giveRUN, emptyDebt, 'RUN'); - manager.mintAndReallocate(toMint, fee, clientSeat, vaultSeat); + stageDelta(clientSeat, vaultSeat, giveRUN, helper.emptyDebt(), 'RUN'); + state.manager.mintAndReallocate(toMint, fee, clientSeat, vaultSeat); // parent needs to know about the change in debt - updateDebtAccounting(debtPre, collateralPre, newDebt); - manager.burnAndRecord(giveRUN, vaultSeat); - assertVaultHoldsNoRun(); + helper.updateDebtAccounting(debtPre, collateralPre, newDebt); + state.manager.burnAndRecord(giveRUN, vaultSeat); + helper.assertVaultHoldsNoRun(); - updateUiState(); + helper.updateUiState(); clientSeat.exit(); return 'We have adjusted your balances, thank you for your business'; - }; + }, - const makeAdjustBalancesInvitation = () => { - assertActive(); - return zcf.makeInvitation(adjustBalancesHook, 'AdjustBalances'); - }; + /** + * + * @param {MethodContext} context + * @param {ZCFSeat} seat + * @returns {VaultKit} + */ + makeTransferInvitationHook: ({ state, facets }, seat) => { + const { self, helper } = facets; + helper.assertCloseable(); + seat.exit(); + // eslint-disable-next-line no-use-before-define + const vaultKit = makeVaultKit(self, state.manager.getNotifier()); + state.outerUpdater = vaultKit.vaultUpdater; + helper.updateUiState(); + + return vaultKit; + }, +}; + +const selfBehavior = { + getVaultSeat: ({ state }) => state.vaultSeat, /** + * @param {MethodContext} context * @param {ZCFSeat} seat - * @param {InnerVault} innerVault */ - const initVaultKit = async (seat, innerVault) => { + initVaultKit: async ({ state, facets }, seat) => { + const { self, helper } = facets; assert( AmountMath.isEmpty(state.debtSnapshot), X`vault must be empty initially`, ); // TODO should this be simplified to know that the oldDebt mut be empty? - const debtPre = getCurrentDebt(); - const collateralPre = getCollateralAmount(); - trace('initVaultKit start: collateral', idInManager, { + const debtPre = self.getCurrentDebt(); + const collateralPre = self.getCollateralAmount(); + trace('initVaultKit start: collateral', state.idInManager, { debtPre, collateralPre, }); @@ -542,7 +516,7 @@ const constructFromState = state => { newDebt: newDebtPre, fee, toMint, - } = loanFee(debtPre, emptyDebt, wantRUN); + } = helper.loanFee(debtPre, helper.emptyDebt(), wantRUN); assert( !AmountMath.isEmpty(fee), X`loan requested (${wantRUN}) is too small; cannot accrue interest`, @@ -550,89 +524,152 @@ const constructFromState = state => { assert(AmountMath.isEqual(newDebtPre, toMint), X`fee mismatch for vault`); trace( 'initVault', - idInManager, + state.idInManager, { wantedRun: wantRUN, fee }, - getCollateralAmount(), + self.getCollateralAmount(), ); - await assertSufficientCollateral(giveCollateral, newDebtPre); + await helper.assertSufficientCollateral(giveCollateral, newDebtPre); const { vaultSeat } = state; vaultSeat.incrementBy( seat.decrementBy(harden({ Collateral: giveCollateral })), ); - manager.mintAndReallocate(toMint, fee, seat, vaultSeat); - updateDebtAccounting(debtPre, collateralPre, newDebtPre); + state.manager.mintAndReallocate(toMint, fee, seat, vaultSeat); + helper.updateDebtAccounting(debtPre, collateralPre, newDebtPre); - const vaultKit = makeVaultKit(innerVault, manager.getNotifier()); + const vaultKit = makeVaultKit(self, state.manager.getNotifier()); state.outerUpdater = vaultKit.vaultUpdater; - updateUiState(); + helper.updateUiState(); return vaultKit; - }; + }, + + liquidating: ({ facets }) => { + const { helper } = facets; + helper.assignPhase(VaultPhase.LIQUIDATING); + helper.updateUiState(); + }, /** + * Call must check for and remember shortfall * - * @param {ZCFSeat} seat - * @returns {VaultKit} + * @param {MethodContext} context + * @param {Amount} newDebt */ - const makeTransferInvitationHook = seat => { - assertCloseable(); - seat.exit(); - // eslint-disable-next-line no-use-before-define - const vaultKit = makeVaultKit(innerVault, manager.getNotifier()); - state.outerUpdater = vaultKit.vaultUpdater; - updateUiState(); - - return vaultKit; - }; - - const innerVault = { - getVaultSeat: () => state.vaultSeat, + liquidated: ({ facets }, newDebt) => { + const { helper } = facets; + helper.updateDebtSnapshot(newDebt); + + helper.assignPhase(VaultPhase.LIQUIDATED); + helper.updateUiState(); + }, + + makeAdjustBalancesInvitation: ({ state, facets }) => { + const { helper } = facets; + helper.assertActive(); + return state.zcf.makeInvitation( + helper.adjustBalancesHook, + 'AdjustBalances', + ); + }, + + makeCloseInvitation: ({ state, facets }) => { + const { helper } = facets; + helper.assertCloseable(); + return state.zcf.makeInvitation(helper.closeHook, 'CloseVault'); + }, + + makeTransferInvitation: ({ state, facets }) => { + const { self, helper } = facets; + // Bring the debt snapshot current for the final report before transfer + helper.updateDebtSnapshot(self.getCurrentDebt()); + const { + outerUpdater, + debtSnapshot: debt, + interestSnapshot: interest, + phase, + } = state; + if (outerUpdater) { + outerUpdater.finish(helper.snapshotState(VaultPhase.TRANSFER)); + state.outerUpdater = null; + } + const transferState = { + debtSnapshot: { debt, interest }, + locked: self.getCollateralAmount(), + vaultState: phase, + }; + return state.zcf.makeInvitation( + helper.makeTransferInvitationHook, + 'TransferVault', + transferState, + ); + }, - initVaultKit: seat => initVaultKit(seat, innerVault), - liquidating, - liquidated, + // for status/debugging - makeAdjustBalancesInvitation, - makeCloseInvitation, - makeTransferInvitation: () => { - // Bring the debt snapshot current for the final report before transfer - updateDebtSnapshot(getCurrentDebt()); - const { - outerUpdater, - debtSnapshot: debt, - interestSnapshot: interest, - phase, - } = state; - if (outerUpdater) { - outerUpdater.finish(snapshotState(VaultPhase.TRANSFER)); - state.outerUpdater = null; - } - const transferState = { - debtSnapshot: { debt, interest }, - locked: getCollateralAmount(), - vaultState: phase, - }; - return zcf.makeInvitation( - makeTransferInvitationHook, - 'TransferVault', - transferState, - ); - }, + /** + * + * @param {MethodContext} context + * @returns {Amount<'nat'>} + */ + getCollateralAmount: ({ state, facets }) => { + const { vaultSeat } = state; + const { helper } = facets; + // getCollateralAllocated would return final allocations + return vaultSeat.hasExited() + ? helper.emptyCollateral() + : helper.getCollateralAllocated(vaultSeat); + }, - // for status/debugging - getCollateralAmount, - getCurrentDebt, - getNormalizedDebt, - }; + /** + * The actual current debt, including accrued interest. + * + * This looks like a simple getter but it does a lot of the heavy lifting for + * interest accrual. Rather than updating all records when interest accrues, + * the vault manager updates just its rolling compounded interest. Here we + * calculate what the current debt is given what's recorded in this vault and + * what interest has compounded since this vault record was written. + * + * @see getNormalizedDebt + * + * @param {MethodContext} context + * @returns {Amount<'nat'>} + */ + getCurrentDebt: ({ state }) => { + return calculateCurrentDebt( + state.debtSnapshot, + state.interestSnapshot, + state.manager.getCompoundedInterest(), + ); + }, - return innerVault; + /** + * The normalization puts all debts on a common time-independent scale since + * the launch of this vault manager. This allows the manager to order vaults + * by their debt-to-collateral ratios without having to mutate the debts as + * the interest accrues. + * + * @see getActualDebAmount + * + * @param {MethodContext} context + * @returns {Amount<'nat'>} as if the vault was open at the launch of this manager, before any interest accrued + */ + getNormalizedDebt: ({ state }) => { + return reverseInterest(state.debtSnapshot, state.interestSnapshot); + }, }; -export const makeInnerVault = defineKind( - 'InnerVault', - initState, - constructFromState, -); +const makeInnerVaultBase = defineKind('InnerVault', initState, { + self: selfBehavior, + helper: helperBehavior, +}); + +/** + * @param {ZCF} zcf + * @param {InnerVaultManagerBase & GetVaultParams} manager + * @param {VaultId} idInManager + */ +export const makeInnerVault = (zcf, manager, idInManager) => + makeInnerVaultBase(zcf, manager, idInManager).self; /** @typedef {ReturnType} InnerVault */ diff --git a/packages/swingset-runner/demo/vatStore1/vat-bob.js b/packages/swingset-runner/demo/vatStore1/vat-bob.js index f8ba643fca0..f53e81fe26b 100644 --- a/packages/swingset-runner/demo/vatStore1/vat-bob.js +++ b/packages/swingset-runner/demo/vatStore1/vat-bob.js @@ -9,29 +9,29 @@ const makeThing = defineKind( p(`@@@ thing.initialize(${label}, ${counter})`); return { counter, label, resetCounter: 0 }; }, - state => ({ - inc() { + { + inc({ state }) { state.counter += 1; p(`#thing# ${state.label} inc() counter now ${state.counter}`); }, - reset(newStart) { + reset({ state }, newStart) { p(`#thing# ${state.label} reset(${newStart})`); state.counter = newStart; state.resetCounter += 1; }, - relabel(newLabel) { + relabel({ state }, newLabel) { p(`#thing# ${state.label} relabel(${newLabel})`); state.label = newLabel; }, - get() { + get({ state }) { p(`#thing# ${state.label} get()=>${state.counter}`); return state.counter; }, - describe() { + describe({ state }) { p(`#thing# ${state.label} describe()`); return `${state.label} counter has been reset ${state.resetCounter} times and is now ${state.counter}`; }, - }), + }, ); const makeZot = defineKind( @@ -40,22 +40,22 @@ const makeZot = defineKind( p(`@@@ zot.initialize(${arbitrary}, ${name}, ${tag})`); return { arbitrary, name, tag, count: 0 }; }, - state => ({ - sayHello(msg) { + { + sayHello({ state }, msg) { p(`#zot# ${msg} ${state.name}`); state.count += 1; }, - rename(newName) { + rename({ state }, newName) { p(`#zot# ${state.name} rename(${newName})`); state.name = newName; state.count += 1; }, - printInfo() { + printInfo({ state }) { // prettier-ignore p(`#zot# ${state.name} tag=${state.tag} count=${state.count} arbitrary=${state.arbitrary}`); state.count += 1; }, - }), + }, ); export function buildRootObject(_vatPowers) { diff --git a/packages/swingset-runner/demo/vatStore2/thingHolder.js b/packages/swingset-runner/demo/vatStore2/thingHolder.js index 713fd9dcffc..b926c0da65b 100644 --- a/packages/swingset-runner/demo/vatStore2/thingHolder.js +++ b/packages/swingset-runner/demo/vatStore2/thingHolder.js @@ -11,28 +11,28 @@ function build(name) { p(`${name}'s thing ${label}: initialize ${companionName}`); return { label, companion, companionName, count: 0 }; }, - state => ({ - echo(message) { + { + echo({ state }, message) { state.count += 1; E(state.companion).say(message); }, - async changePartner(newCompanion) { + async changePartner({ state }, newCompanion) { state.count += 1; state.companion = newCompanion; const companionName = await E(newCompanion).getName(); state.companionName = companionName; p(`${name}'s thing ${state.label}: changePartner ${companionName}`); }, - getLabel() { + getLabel({ state }) { const label = state.label; p(`${name}'s thing ${label}: getLabel`); state.count += 1; return label; }, - report() { + report({ state }) { p(`${name}'s thing ${state.label} invoked ${state.count} times`); }, - }), + }, ); let nextThingNumber = 0; diff --git a/packages/swingset-runner/demo/vatStore3/vat-bob.js b/packages/swingset-runner/demo/vatStore3/vat-bob.js index 0f6fa6d1840..a329b089992 100644 --- a/packages/swingset-runner/demo/vatStore3/vat-bob.js +++ b/packages/swingset-runner/demo/vatStore3/vat-bob.js @@ -5,13 +5,9 @@ import { defineKind } from '@agoric/vat-data'; const things = []; export function buildRootObject(_vatPowers) { - const makeThing = defineKind( - 'thing', - label => ({ label }), - state => ({ - getLabel: () => state.label, - }), - ); + const makeThing = defineKind('thing', label => ({ label }), { + getLabel: ({ state }) => state.label, + }); let nextThingNumber = 0; diff --git a/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js b/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js index 8cb4314dc5a..5c364395c5a 100644 --- a/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js +++ b/packages/swingset-runner/demo/virtualObjectGC/vat-bob.js @@ -3,21 +3,13 @@ import { Far } from '@endo/marshal'; import { defineKind } from '@agoric/vat-data'; export function buildRootObject(_vatPowers) { - const makeThing = defineKind( - 'thing', - label => ({ label }), - state => ({ - getLabel: () => state.label, - }), - ); + const makeThing = defineKind('thing', label => ({ label }), { + getLabel: ({ state }) => state.label, + }); - const makeVirtualHolder = defineKind( - 'holder', - value => ({ value }), - state => ({ - getValue: () => state.value, - }), - ); + const makeVirtualHolder = defineKind('holder', value => ({ value }), { + getValue: ({ state }) => state.value, + }); const cacheDisplacer = makeThing('cacheDisplacer'); let nextThingNumber = 0; diff --git a/packages/vat-data/src/index.test-d.ts b/packages/vat-data/src/index.test-d.ts index 5a1eeb23cdf..0d33b55c66a 100644 --- a/packages/vat-data/src/index.test-d.ts +++ b/packages/vat-data/src/index.test-d.ts @@ -1,5 +1,8 @@ import { defineKind } from '.'; +import type { MinusContext } from './types'; + +/* export const makePaymentMaker = (allegedName: string, brand: unknown) => { const makePayment = defineKind( `${allegedName} payment`, @@ -22,3 +25,44 @@ const f = makeFlorg(42); f.concat; // string // @ts-expect-error makeFlorg('notnumber'); +*/ + +// From virtual-objects.md +type CounterContext = { + state: { + counter: number; + }; +}; +const initFacetedCounter = () => ({ counter: 0 }); +const getCount = ({ state }: CounterContext) => state.counter; +const facetedCounterBehavior = { + incr: { + step: ({ state }) => { + state.counter += 1; + }, + getCount, + }, + decr: { + step: ({ state }) => { + state.counter -= 1; + }, + getCount, + echo: (context, toEcho) => toEcho, + }, +}; + +const makeFacetedCounter = defineKind( + 'counter', + initFacetedCounter, + facetedCounterBehavior, +); + +const fc = makeFacetedCounter(); +fc.incr.step(); +fc.decr.step(); +fc.decr.getCount(); +// @ts-expect-error missing argument +fc.decr.echo(); +fc.decr.echo('foo'); +// @ts-expect-error missing method +fc.incr.echo('foo'); diff --git a/packages/vat-data/src/types.d.ts b/packages/vat-data/src/types.d.ts index cca2ad8f327..ace1e1cd854 100644 --- a/packages/vat-data/src/types.d.ts +++ b/packages/vat-data/src/types.d.ts @@ -5,14 +5,27 @@ import type { WeakMapStore, WeakSetStore, } from '@agoric/store'; +import { Context } from 'vm'; + +type Tail = T extends [head: any, ...tail: infer Tail_] + ? Tail_ + : never; + +type MinusContext< + F extends (context, ...rest: any[]) => any, + P extends any[] = Parameters, // P: are the parameters of F + R = ReturnType, // R: the return type of F +> = (...args: Tail

) => R; + +type FunctionsMinusContext = { [K in keyof O]: MinusContext }; interface KindDefiner { - ( + ( tag: string, init: (...args: P) => S, - actualize: (state: S) => K, + behavior: B, finish?: () => void, - ): (...args: P) => K; + ): (...args: P) => { [Facet in keyof B]: FunctionsMinusContext }; } export type VatData = { diff --git a/packages/vat-data/test/present.test.js b/packages/vat-data/test/present.test.js index a9e4733cc15..4ed5850553a 100644 --- a/packages/vat-data/test/present.test.js +++ b/packages/vat-data/test/present.test.js @@ -2,10 +2,6 @@ import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; test('methods available', async t => { const { defineKind } = await import('../src/index.js'); - defineKind( - 'test', - () => {}, - () => {}, - ); + defineKind('test', () => {}, {}); t.pass(); }); diff --git a/packages/zoe/test/minimalMakeKindContract.js b/packages/zoe/test/minimalMakeKindContract.js index e50ddb15067..c7030c0889d 100644 --- a/packages/zoe/test/minimalMakeKindContract.js +++ b/packages/zoe/test/minimalMakeKindContract.js @@ -1,9 +1,9 @@ /* global VatData */ const start = _zcf => { - VatData.defineKind(); + VatData.defineKind('x', () => {}, {}); const kh = VatData.makeKindHandle(); - VatData.defineDurableKind(kh); + VatData.defineDurableKind(kh, () => {}, {}); VatData.makeScalarBigMapStore(); VatData.makeScalarBigWeakMapStore(); VatData.makeScalarBigSetStore(); diff --git a/packages/zoe/test/unitTests/test-makeKind.js b/packages/zoe/test/unitTests/test-makeKind.js index 62bdf8549db..42361a90d54 100644 --- a/packages/zoe/test/unitTests/test-makeKind.js +++ b/packages/zoe/test/unitTests/test-makeKind.js @@ -21,10 +21,10 @@ test('defineKind non-swingset', async t => { const { zoeService: zoe } = makeZoeKit(fakeVatAdmin); vatAdminState.installBundle('b1-minimal', bundle); const installation = await E(zoe).installBundleID('b1-minimal'); - t.notThrows(() => VatData.defineKind()); + t.notThrows(() => VatData.defineKind('x', () => {}, {})); t.notThrows(() => VatData.makeKindHandle()); const kh = VatData.makeKindHandle(); - t.notThrows(() => VatData.defineDurableKind(kh)); + t.notThrows(() => VatData.defineDurableKind(kh, () => {}, {})); t.notThrows(() => VatData.makeScalarBigMapStore()); t.notThrows(() => VatData.makeScalarBigWeakMapStore()); t.notThrows(() => VatData.makeScalarBigSetStore()); diff --git a/packages/zoe/test/unitTests/test-zoe-env.js b/packages/zoe/test/unitTests/test-zoe-env.js index c123f394615..95e0ec943fc 100644 --- a/packages/zoe/test/unitTests/test-zoe-env.js +++ b/packages/zoe/test/unitTests/test-zoe-env.js @@ -10,9 +10,9 @@ test('harden from SES is in the zoe contract environment', t => { test('(mock) kind makers from SwingSet are in the zoe contract environment', t => { // @ts-ignore testing existence of function only - VatData.defineKind(); + VatData.defineKind('x', () => {}, {}); const kh = VatData.makeKindHandle(); - VatData.defineDurableKind(kh); + VatData.defineDurableKind(kh, () => {}, {}); t.pass(); });