Skip to content

Commit

Permalink
feat(liveslots): virtual exo meta-ops
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Mar 26, 2024
1 parent 247cc9b commit ca1c824
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 20 deletions.
41 changes: 35 additions & 6 deletions packages/swingset-liveslots/src/vatDataTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,41 @@ export type DefineKindOptions<C> = {
finish?: (context: C) => void;

/**
* If provided, it describes the shape of all state records of instances
* of this kind.
*/
stateShape?: StateShape;

/**
* If a `receiveAmplifier` function is provided, it will be called during
* definition of the exo class kit with an `Amplify` function. If called
* during the definition of a normal exo or exo class, it will throw, since
* only exo kits can be amplified.
* An `Amplify` function is a function that takes a facet instance of
* this class kit as an argument, in which case it will return the facets
* record, giving access to all the facet instances of the same cohort.
*/
receiveAmplifier?: ReceivePower<Amplify<F>>;

/**
* If a `receiveInstanceTester` function is provided, it will be called
* during the definition of the exo class or exo class kit with an
* `IsInstance` function. The first argument of `IsInstance`
* is the value to be tested. When it may be a facet instance of an
* exo class kit, the optional second argument, if provided, is
* a `facetName`. In that case, the function tests only if the first
* argument is an instance of that facet of the associated exo class kit.
*/
receiveInstanceTester?: ReceivePower<IsInstance>;

// TODO properties above are identical to those in FarClassOptions.
// These are the only options that should be exposed by
// vat-data's public virtual/durable exo APIs. This DefineKindOptions
// should explicitly be a subtype, where the methods below are only for
// internal use, i.e., below the exo level.

/**
* As a kind option, intended for internal use only.
* Meaningful to `makeScalarBigMapStore` and its siblings. These maker
* fuctions will make either virtual or durable stores, depending on
* this flag. Defaults to off, making virtual but not durable collections.
Expand All @@ -84,12 +119,6 @@ export type DefineKindOptions<C> = {
*/
durable?: boolean;

/**
* If provided, it describes the shape of all state records of instances
* of this kind.
*/
stateShape?: StateShape;

/**
* Intended for internal use only.
* Should the raw methods receive their `context` argument as their first
Expand Down
105 changes: 94 additions & 11 deletions packages/swingset-liveslots/src/virtualObjectManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ import {
* @typedef {import('@endo/exo/src/exo-tools.js').KitContextProvider } KitContextProvider
*/

const { hasOwn, defineProperty, getOwnPropertyNames, entries, fromEntries } =
Object;
/**
*
*/

const {
hasOwn,
defineProperty,
getOwnPropertyNames,
values,
entries,
fromEntries,
} = Object;
const { ownKeys } = Reflect;

// Turn on to give each exo instance its own toStringTag value which exposes
Expand Down Expand Up @@ -679,6 +689,8 @@ export const makeVirtualObjectManager = (
const {
finish = undefined,
stateShape = undefined,
receiveAmplifier = undefined,
receiveInstanceTester = undefined,
thisfulMethods = false,
} = options;
let {
Expand Down Expand Up @@ -766,6 +778,11 @@ export const makeVirtualObjectManager = (
Fail`A stateShape must be a copyRecord: ${q(stateShape)}`;
assertPattern(stateShape);

if (!multifaceted) {
receiveAmplifier === undefined ||
Fail`Only facets of an exo class kit can be amplified ${q(tag)}`;
}

let facetNames;

if (isDurable) {
Expand Down Expand Up @@ -948,14 +965,20 @@ export const makeVirtualObjectManager = (
// and into method-invocation time (which is not).

let proto;
/** @type {ClassContextProvider | undefined} */
let contextProviderVar;
/** @type { Record<string, KitContextProvider> | undefined } */
let contextProviderKitVar;

if (multifaceted) {
/** @type { Record<string, KitContextProvider> } */
const contextProviderKit = fromEntries(
contextProviderKitVar = fromEntries(
facetNames.map((name, index) => [
name,
rep => {
const vref = getSlotForVal(rep);
assert(vref !== undefined);
if (vref === undefined) {
return undefined;
}
const { baseRef, facet } = parseVatSlot(vref);

// Without this check, an attacker (with access to both
Expand All @@ -966,7 +989,9 @@ export const makeVirtualObjectManager = (
// objects, but they could invoke all their equivalent methods,
// by using e.g.
// cohort1.facetA.foo.apply(cohort2.facetB, [...args])
Number(facet) === index || Fail`illegal cross-facet access`;
if (Number(facet) !== index) {
return undefined;
}

return harden(contextCache.get(baseRef));
},
Expand All @@ -975,28 +1000,33 @@ export const makeVirtualObjectManager = (

proto = defendPrototypeKit(
tag,
harden(contextProviderKit),
harden(contextProviderKitVar),
behavior,
thisfulMethods,
interfaceGuardKit,
);
} else {
/** @type {ClassContextProvider} */
const contextProvider = rep => {
contextProviderVar = rep => {
const slot = getSlotForVal(rep);
assert(slot !== undefined);
if (slot === undefined) {
return undefined;
}
return harden(contextCache.get(slot));
};
proto = defendPrototype(
tag,
harden(contextProvider),
harden(contextProviderVar),
behavior,
thisfulMethods,
interfaceGuard,
);
}
harden(proto);

// All this to let typescript know that it won't vary during a closure
const contextProvider = contextProviderVar;
const contextProviderKit = contextProviderKitVar;

// this builds new Representatives, both when creating a new instance and
// for reanimating an existing one when the old rep gets GCed

Expand Down Expand Up @@ -1074,6 +1104,59 @@ export const makeVirtualObjectManager = (
return val;
};

if (receiveAmplifier) {
assert(contextProviderKit);

// Amplify a facet to a cohort
const amplify = exoFacet => {
for (const cp of values(contextProviderKit)) {
const context = cp(exoFacet);
if (context !== undefined) {
return context.facets;
}
}
throw Fail`Must be a facet of ${q(tag)}: ${exoFacet}`;
};
harden(amplify);
receiveAmplifier(amplify);
}

if (receiveInstanceTester) {
if (multifaceted) {
assert(contextProviderKit);

const isInstance = (exoFacet, facetName = undefined) => {
if (facetName === undefined) {
// Is exoFacet and instance of any facet of this class kit?
return values(contextProviderKit).some(
cp => cp(exoFacet) !== undefined,
);
}
// Is this exoFacet an instance of this specific facet column
// of this class kit?
assert.typeof(facetName, 'string');
const cp = contextProviderKit[facetName];
cp !== undefined ||
Fail`exo class kit ${q(tag)} has no facet named ${q(facetName)}`;
return cp(exoFacet) !== undefined;
};
harden(isInstance);
receiveInstanceTester(isInstance);
} else {
assert(contextProvider);
// Is this exo an instance of this class?
const isInstance = (exo, facetName = undefined) => {
facetName === undefined ||
Fail`facetName can only be used with an exo class kit: ${q(
tag,
)} has no facet ${q(facetName)}`;
return contextProvider(exo) !== undefined;
};
harden(isInstance);
receiveInstanceTester(isInstance);
}
}

return makeNewInstance;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ test('forbid cross-facet prototype attack', t => {
thing2.mutable.set(2);

t.throws(() => attack1(thing1.mutable, thing2.immutable), {
message: /^illegal cross-facet access/,
message:
'"In \\"set\\" method of (thing mutable)" may only be applied to a valid instance: "[Alleged: thing immutable]"',
});
t.throws(() => attack2(thing1.mutable, thing2.immutable), {
message: /^illegal cross-facet access/,
message:
'"In \\"set\\" method of (thing mutable)" may only be applied to a valid instance: "[Alleged: thing immutable]"',
});
t.is(thing1.immutable.get(), 1);
t.is(thing2.immutable.get(), 2);
Expand Down
4 changes: 3 additions & 1 deletion packages/vat-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"@agoric/internal": "^0.3.2",
"@agoric/store": "^0.9.2",
"@agoric/swingset-liveslots": "^0.10.2",
"@agoric/vow": "^0.1.0"
"@agoric/vow": "^0.1.0",
"@endo/exo": "^1.1.0",
"@endo/patterns": "^1.1.0"
},
"devDependencies": {
"@endo/init": "^1.1.0",
Expand Down
89 changes: 89 additions & 0 deletions packages/vat-data/test/test-amplify-virtual-class-kits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// modeled on test-amplify-heap-class-kits.js
import { test } from './prepare-test-env-ava.js';

// eslint-disable-next-line import/order
import { M } from '@endo/patterns';
import {
defineVirtualExoClass,
defineVirtualExoClassKit,
} from '../src/exo-utils.js';

const UpCounterI = M.interface('UpCounter', {
incr: M.call().optional(M.gte(0)).returns(M.number()),
});

const DownCounterI = M.interface('DownCounter', {
decr: M.call().optional(M.gte(0)).returns(M.number()),
});

test('test amplify defineVirtualExoClass fails', t => {
t.throws(
() =>
defineVirtualExoClass(
'UpCounter',
UpCounterI,
/** @param {number} [x] */
(x = 0) => ({ x }),
{
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
{
receiveAmplifier(_) {},
},
),
{
message: 'Only facets of an exo class kit can be amplified "UpCounter"',
},
);
});

test('test amplify defineVirtualExoClassKit', t => {
/** @type {import('@endo/exo/src/exo-makers.js').Amplify} */
let amp;
const makeCounterKit = defineVirtualExoClassKit(
'Counter',
{ up: UpCounterI, down: DownCounterI },
/** @param {number} [x] */
(x = 0) => ({ x }),
{
up: {
incr(y = 1) {
const { state } = this;
state.x += y;
return state.x;
},
},
down: {
decr(y = 1) {
const { state } = this;
state.x -= y;
return state.x;
},
},
},
{
receiveAmplifier(a) {
amp = a;
},
},
);
// @ts-expect-error TS thinks it is used before assigned, which is a hazard
// TS is correct to bring to our attention, since there is not enough static
// into to infer otherwise.
assert(amp !== undefined);

const counterKit = makeCounterKit(3);
const { up: upCounter, down: downCounter } = counterKit;
t.is(upCounter.incr(5), 8);
t.is(downCounter.decr(), 7);

t.throws(() => amp(harden({})), {
message: 'Must be a facet of "Counter": {}',
});
t.deepEqual(amp(upCounter), counterKit);
t.deepEqual(amp(downCounter), counterKit);
});
Loading

0 comments on commit ca1c824

Please sign in to comment.