diff --git a/packages/ERTP/src/interfaces.js b/packages/ERTP/src/interfaces.js index 35ebaf68ca78..6e31ead19f1c 100644 --- a/packages/ERTP/src/interfaces.js +++ b/packages/ERTP/src/interfaces.js @@ -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( { @@ -10,10 +9,10 @@ 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 = { @@ -21,25 +20,25 @@ export const MatchAmount = { 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), }); diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index 33331690d617..da2ed22be9d7 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -509,4 +509,4 @@ export const vivifyPaymentLedger = ( }; harden(vivifyPaymentLedger); -/** @typedef {ReturnType} PaymentLedger */ +/** @typedef {ReturnType} PaymentLedger */ diff --git a/packages/store/src/index.js b/packages/store/src/index.js index c54a88363872..07fe0146de32 100755 --- a/packages/store/src/index.js +++ b/packages/store/src/index.js @@ -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, diff --git a/packages/store/src/patterns/defineHeapKind.js b/packages/store/src/patterns/defineHeapKind.js new file mode 100644 index 000000000000..1c7af8bca716 --- /dev/null +++ b/packages/store/src/patterns/defineHeapKind.js @@ -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); diff --git a/packages/store/src/patterns/interface-tools.js b/packages/store/src/patterns/interface-tools.js new file mode 100644 index 000000000000..a8ae2b77d785 --- /dev/null +++ b/packages/store/src/patterns/interface-tools.js @@ -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); diff --git a/packages/store/test/test-interface-tools.js b/packages/store/test/test-interface-tools.js new file mode 100644 index 000000000000..83dbed5a45bb --- /dev/null +++ b/packages/store/test/test-interface-tools.js @@ -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$/, + }); +}); diff --git a/packages/vat-data/src/kind-utils.js b/packages/vat-data/src/kind-utils.js index fe2f21684f4c..2262b784eb3f 100644 --- a/packages/vat-data/src/kind-utils.js +++ b/packages/vat-data/src/kind-utils.js @@ -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 {(fn: T) => import('./types.js').PlusContext} */ export const ignoreContext = fn => - (context, ...args) => + (_context, ...args) => fn(...args); // @ts-expect-error TODO statically recognize harden harden(ignoreContext); diff --git a/patches/@endo+marshal+0.6.9.patch b/patches/@endo+marshal+0.6.9.patch new file mode 100644 index 000000000000..7cb9b4b8778c --- /dev/null +++ b/patches/@endo+marshal+0.6.9.patch @@ -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(