Skip to content

Commit

Permalink
fix: interface guards
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Aug 15, 2022
1 parent 8f9587c commit 49d520a
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 23 deletions.
39 changes: 19 additions & 20 deletions packages/ERTP/src/interfaces.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable no-use-before-define */
/* global foo */
const M = foo();
import { M, I } from '@agoric/store';

export const MatchDisplayInfo = M.rest(
{
Expand All @@ -10,36 +9,36 @@ export const MatchDisplayInfo = M.rest(
}),
);

export const BrandI = M.interface({
isMyIssuer: M.callWhen(M.await(IssuerI)).returns(M.boolean()),
getAllegedName: M.call().returns(M.string()),
getDisplayInfo: M.call().returns(MatchDisplayInfo),
export const BrandI = I.interface('Brand', {
isMyIssuer: I.callWhen(I.await(IssuerI)).returns(M.boolean()),
getAllegedName: I.call().returns(M.string()),
getDisplayInfo: I.call().returns(MatchDisplayInfo),
});

export const MatchAmount = {
brand: BrandI,
value: M.or(M.bigint(), M.array()),
};

export const IssuerI = M.interface({
getBrand: M.call().returns(BrandI),
getAllegedName: M.call().returns(M.string()),
getAssetKind: M.call().returns(M.or('nat', 'set')),
getDisplayInfo: M.call().returns(MatchDisplayInfo),
makeEmptyPurse: M.call().returns(PurseI),
export const IssuerI = I.interface('Issuer', {
getBrand: I.call().returns(BrandI),
getAllegedName: I.call().returns(M.string()),
getAssetKind: I.call().returns(M.or('nat', 'set')),
getDisplayInfo: I.call().returns(MatchDisplayInfo),
makeEmptyPurse: I.call().returns(PurseI),

isLive: M.callWhen(M.await(PaymentI)).returns(M.boolean()),
getAmountOf: M.callWhen(M.await(PaymentI)).returns(MatchAmount),
isLive: I.callWhen(I.await(PaymentI)).returns(M.boolean()),
getAmountOf: I.callWhen(I.await(PaymentI)).returns(MatchAmount),
});

export const PaymentI = M.interface({
getAllegedBrand: M.call().returns(BrandI),
export const PaymentI = I.interface('Payment', {
getAllegedBrand: I.call().returns(BrandI),
});

export const PurseI = M.interface({
getAllegedBrand: M.call().returns(BrandI),
deposit: M.apply(M.rest([PaymentI]).optionals([MatchAmount])).returns(
export const PurseI = I.interface('Purse', {
getAllegedBrand: I.call().returns(BrandI),
deposit: I.apply(M.rest([PaymentI], M.partial([MatchAmount]))).returns(
MatchAmount,
),
withdraw: M.call(MatchAmount).returns(PaymentI),
withdraw: I.call(MatchAmount).returns(PaymentI),
});
2 changes: 1 addition & 1 deletion packages/ERTP/src/paymentLedger.js
Original file line number Diff line number Diff line change
Expand Up @@ -509,4 +509,4 @@ export const vivifyPaymentLedger = (
};
harden(vivifyPaymentLedger);

/** @typedef {ReturnType<vivifyPaymentLedger>} PaymentLedger */
/** @typedef {ReturnType<typeof vivifyPaymentLedger>} PaymentLedger */
1 change: 1 addition & 0 deletions packages/store/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export {
matches,
fit,
} from './patterns/patternMatchers.js';
export { I } from './patterns/interface-tools.js';
export { compareRank, isRankSorted, sortByRank } from './patterns/rankOrder.js';
export {
makeDecodePassable,
Expand Down
37 changes: 37 additions & 0 deletions packages/store/src/patterns/defineHeapKind.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { makeLegacyWeakMap } from '../legacy/legacyWeakMap.js';
import { defendVTable } from './interface-tools.js';

const { create, freeze, seal } = Object;

export const defineHeapKind = (
ifaceGuard,
init,
rawVTable,
{ finish = undefined } = {},
) => {
if (typeof ifaceGuard === 'string') {
ifaceGuard = harden({
klass: 'Interface',
farName: ifaceGuard,
methodGuards: {},
});
}
const { klass } = ifaceGuard;
assert(klass === 'Interface');
// legacyWeakMap to avoid hardening state
const contextMapStore = makeLegacyWeakMap();
const defensiveVTable = defendVTable(rawVTable, contextMapStore, ifaceGuard);
const makeInstance = (...args) => {
// Don't freeze state
const state = seal(init(...args));
const self = harden(create(defensiveVTable));
const context = freeze({ state, self });
contextMapStore.init(self, context);
if (finish) {
finish(context);
}
return self;
};
return harden(makeInstance);
};
harden(defineHeapKind);
179 changes: 179 additions & 0 deletions packages/store/src/patterns/interface-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { PASS_STYLE, assertRemotable } from '@endo/marshal';
import { E } from '@endo/eventual-send';
import { M, fit } from './patternMatchers.js';

const { details: X, quote: q } = assert;
const { apply, ownKeys } = Reflect;
const { fromEntries, entries, create, setPrototypeOf, defineProperties } =
Object;

const makeMethodGuardMaker = (callKind, argGuards) =>
harden({
returns: (returnGuard = M.undefined()) =>
harden({
klass: 'methodGuard',
callKind,
argGuards,
returnGuard,
}),
});

const makeAwaitArgGuard = argGuard =>
harden({
klass: 'awaitArg',
argGuard,
});

const isAwaitArgGuard = argGuard =>
argGuard && typeof argGuard === 'object' && argGuard.klass === 'awaitArg';

export const I = harden({
interface: (farName, methodGuards) => {
for (const [_, methodGuard] of entries(methodGuards)) {
assert(
methodGuard.klass === 'methodGuard',
X`unrecognize method guard ${methodGuard}`,
);
}
return harden({
klass: 'Interface',
farName,
methodGuards,
});
},
call: (...argGuards) => makeMethodGuardMaker('sync', argGuards),
callWhen: (...argGuards) => makeMethodGuardMaker('async', argGuards),
apply: argGuards => makeMethodGuardMaker('sync', argGuards),
applyWhen: argGuards => makeMethodGuardMaker('async', argGuards),

await: argGuard => makeAwaitArgGuard(argGuard),
});

const mimicMethodNameLength = (defensiveMethod, rawMethod) => {
defineProperties(defensiveMethod, {
name: { value: rawMethod.name },
length: { value: rawMethod.length - 1 },
});
};

const defendSyncMethod = (rawMethod, contextMapStore, methodGuard, label) => {
const { argGuards, returnGuard } = methodGuard;

const { defensiveSyncMethod } = {
// Note purposeful use of `this` and concise method syntax
defensiveSyncMethod(...args) {
assert(
contextMapStore.has(this),
X`method can only be used on its own instances: ${rawMethod}`,
);
const context = contextMapStore.get(this);
fit(harden(args), argGuards, `${label}: args`);
const result = apply(rawMethod, undefined, [context, ...args]);
fit(result, returnGuard, `${label}: result`);
return result;
},
};
mimicMethodNameLength(defensiveSyncMethod, rawMethod);
return harden(defensiveSyncMethod);
};

const defendAsyncMethod = (rawMethod, contextMapStore, methodGuard, label) => {
const { argGuards, returnGuard } = methodGuard;

const rawArgGuards = [];
const awaitIndexes = [];
for (let i = 0; i < argGuards.length; i += 1) {
const argGuard = argGuards[i];
if (isAwaitArgGuard(argGuard)) {
rawArgGuards.push(argGuard.argGuard);
awaitIndexes.push(i);
} else {
rawArgGuards.push(argGuard);
}
}
harden(rawArgGuards);
harden(awaitIndexes);
const { defensiveAsyncMethod } = {
// Note purposeful use of `this` and concise method syntax
defensiveAsyncMethod(...args) {
assert(
contextMapStore.has(this),
X`method can only be used on its own instances: ${rawMethod}`,
);
const context = contextMapStore.get(this);
const awaitList = awaitIndexes.map(i => args[i]);
const p = Promise.all(awaitList);
const rawArgs = [...args];
return E.when(p, awaitedArgs => {
for (let j = 0; j < awaitIndexes.length; j += 1) {
rawArgs[awaitIndexes[j]] = awaitedArgs[j];
}
fit(harden(rawArgs), rawArgGuards, `${label}: args`);
const resultP = apply(rawMethod, undefined, [context, ...rawArgs]);
return E.when(resultP, result => {
fit(result, returnGuard, `${label}: result`);
return result;
});
});
},
};
mimicMethodNameLength(defensiveAsyncMethod, rawMethod);
return harden(defensiveAsyncMethod);
};

const defendMethod = (rawMethod, contextMapStore, methodGuard, label) => {
const { klass, callKind } = methodGuard;
assert(klass === 'methodGuard');

if (callKind === 'sync') {
return defendSyncMethod(rawMethod, contextMapStore, methodGuard, label);
} else {
assert(callKind === 'async');
return defendAsyncMethod(rawMethod, contextMapStore, methodGuard, label);
}
};

const defaultMethodGuard = I.apply(M.array()).returns();

export const defendVTable = (rawVTable, contextMapStore, iface) => {
const { klass, farName, methodGuards } = iface;
assert(klass === 'Interface');
assert.typeof(farName, 'string');

const methodGuardNames = ownKeys(methodGuards);
for (const methodGuardName of methodGuardNames) {
assert(
methodGuardName in rawVTable,
X`${q(methodGuardName)} not implemented by ${rawVTable}`,
);
}
const methodNames = ownKeys(rawVTable);
// like Object.entries, but unenumerable and symbol as well.
const rawMethodEntries = methodNames.map(mName => [mName, rawVTable[mName]]);
const defensiveMethodEntries = rawMethodEntries.map(([mName, rawMethod]) => {
const methodGuard = methodGuards[mName] || defaultMethodGuard;
const defensiveMethod = defendMethod(
rawMethod,
contextMapStore,
methodGuard,
`${farName}.${mName}`,
);
return [mName, defensiveMethod];
});
// Return the defensive VTable, which can be use on a shared
// prototype and shared by instances, avoiding the per-object-per-method
// allocation cost of the objects as closure pattern. That's why we
// use `this` above. To make it safe, each defensive method starts with
// a fail-fast brand check on `this`, ensuring that the methods can only be
// applied to legitimate instances.
const defensiveVTable = fromEntries(defensiveMethodEntries);
const remotableProto = create(Object.prototype, {
[PASS_STYLE]: { value: 'remotable' },
[Symbol.toStringTag]: { value: `Alleged: ${farName}` },
});
setPrototypeOf(defensiveVTable, remotableProto);
harden(defensiveVTable);
assertRemotable(defensiveVTable);
return defensiveVTable;
};
harden(defendVTable);
31 changes: 31 additions & 0 deletions packages/store/test/test-interface-tools.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @ts-check

import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js';
import { passStyleOf } from '@endo/marshal';

import { I } from '../src/patterns/interface-tools.js';
import { defineHeapKind } from '../src/patterns/defineHeapKind.js';
import { M } from '../src/patterns/patternMatchers.js';

test('how far-able is defineHeapKind', t => {
const bobIFace = I.interface('bob', {
foo: I.call(M.number()).returns(M.undefined()),
});
const makeBob = defineHeapKind(bobIFace, field => ({ field }), {
foo: ({ state, self }, carol) => {
t.is(state.field, 8);
t.is(typeof self.foo, 'function');
t.is(self.foo.name, 'foo');
t.is(self.foo.length, 1);
t.is(carol, 77);
state.field += carol;
t.is(state.field, 85);
},
});
const bob = makeBob(8);
t.is(passStyleOf(bob), 'remotable');
bob.foo(77);
t.throws(() => bob.foo(true), {
message: /^bob.foo: args: \[0\]: boolean true - Must be a number$/,
});
});
5 changes: 3 additions & 2 deletions packages/vat-data/src/kind-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import {
const { entries, fromEntries } = Object;

/**
* Make a version of the argument function that takes a kind context but ignores it.
* Make a version of the argument function that takes a kind context but
* ignores it.
*
* @type {<T extends Function>(fn: T) => import('./types.js').PlusContext<never, T>}
*/
export const ignoreContext =
fn =>
(context, ...args) =>
(_context, ...args) =>
fn(...args);
// @ts-expect-error TODO statically recognize harden
harden(ignoreContext);
Expand Down
36 changes: 36 additions & 0 deletions patches/@endo+marshal+0.6.9.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
diff --git a/node_modules/@endo/marshal/src/helpers/remotable.js b/node_modules/@endo/marshal/src/helpers/remotable.js
index c19adca..645bf10 100644
--- a/node_modules/@endo/marshal/src/helpers/remotable.js
+++ b/node_modules/@endo/marshal/src/helpers/remotable.js
@@ -77,6 +77,22 @@ const checkRemotableProtoOf = (original, check = x => x) => {
* }}
*/
const proto = getPrototypeOf(original);
+
+ // From agoric-sdk patch in anticipation of
+ // https://github.com/endojs/endo/pull/1251
+ // TODO: once agoric-sdk is upgraded to depend on that PR, remove this patch.
+ const protoProto = getPrototypeOf(proto);
+ if (
+ typeof original === 'object' &&
+ proto !== objectPrototype &&
+ protoProto !== objectPrototype
+ ) {
+ return (
+ // eslint-disable-next-line no-use-before-define
+ RemotableHelper.canBeValid(proto, check) && checkRemotable(proto, check)
+ );
+ }
+
if (
!(
check(
@@ -95,8 +111,6 @@ const checkRemotableProtoOf = (original, check = x => x) => {
return false;
}

- const protoProto = getPrototypeOf(proto);
-
if (typeof original === 'object') {
if (
!check(

0 comments on commit 49d520a

Please sign in to comment.