From d5101b8b1b46b15d685a1454f6f10409ed605746 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 29 Sep 2022 00:30:17 -0700 Subject: [PATCH] fix: eventual auxdata --- packages/ERTP/src/paymentLedger.js | 10 ++ packages/ERTP/src/typeGuards.js | 8 + packages/ERTP/src/types-ambient.js | 9 + .../test/unitTests/mathHelpers/mockBrand.js | 20 ++- .../mathHelpers/test-natMathHelpers.js | 154 ++++-------------- packages/store/src/types.js | 32 +++- packages/swingset-liveslots/src/liveslots.js | 80 +++++++-- packages/zoe/src/issuerStorage.js | 9 +- packages/zoe/test/unitTests/test-zoe.js | 8 + packages/zoe/test/unitTests/zcf/test-zcf.js | 10 +- 10 files changed, 182 insertions(+), 158 deletions(-) diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index a3b4fbf7b490..e5eb0cab2d01 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -96,6 +96,9 @@ export const preparePaymentLedger = ( getAmountShape() { return amountShape; }, + aux() { + return brandAuxData; + }, }); const emptyAmount = AmountMath.makeEmpty(brand, assetKind); @@ -105,6 +108,13 @@ export const preparePaymentLedger = ( elementShape, ); + const brandAuxData = harden({ + name, + assetKind, + displayInfo, + amountShape, + }); + const { IssuerI, MintI, PaymentI, PurseIKit } = makeIssuerInterfaces( brand, assetKind, diff --git a/packages/ERTP/src/typeGuards.js b/packages/ERTP/src/typeGuards.js index bb8c6d0db449..c7ae85ff9cb4 100644 --- a/packages/ERTP/src/typeGuards.js +++ b/packages/ERTP/src/typeGuards.js @@ -153,11 +153,19 @@ export const IssuerKitShape = harden({ // //////////////////////// Interfaces ///////////////////////////////////////// +export const BrandAuxDataShape = harden({ + name: M.string(), + assetKind: AssetKindShape, + displayInfo: DisplayInfoShape, + amountShape: M.pattern(), +}); + export const BrandI = M.interface('Brand', { isMyIssuer: M.callWhen(M.await(IssuerShape)).returns(M.boolean()), getAllegedName: M.call().returns(M.string()), getDisplayInfo: M.call().returns(DisplayInfoShape), getAmountShape: M.call().returns(M.pattern()), + aux: M.call().returns(BrandAuxDataShape), }); /** diff --git a/packages/ERTP/src/types-ambient.js b/packages/ERTP/src/types-ambient.js index 6b292caf3665..ee9681d513d4 100644 --- a/packages/ERTP/src/types-ambient.js +++ b/packages/ERTP/src/types-ambient.js @@ -83,6 +83,14 @@ * AssetKind.SET or AssetKind.COPY_SET (non-fungible) */ +/** + * @typedef {object} BrandAuxData + * @property {string} name + * @property {AssetKind} assetKind + * @property {DisplayInfo} displayInfo + * @property {Pattern} amountShape + */ + /** * @template {AssetKind} [K=AssetKind] * @typedef {object} Brand @@ -102,6 +110,7 @@ * @property {() => DisplayInfo} getDisplayInfo * Give information to UI on how to display the amount. * @property {() => Pattern} getAmountShape + * @property {() => BrandAuxData} aux */ // /////////////////////////// Issuer ////////////////////////////////////////// diff --git a/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js b/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js index 3b0dd05597f0..a63206f34e29 100644 --- a/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js +++ b/packages/ERTP/test/unitTests/mathHelpers/mockBrand.js @@ -1,13 +1,19 @@ import { Far } from '@endo/marshal'; +import { M } from '@agoric/store'; import { AssetKind } from '../../../src/index.js'; +const mockAuxData = harden({ + name: 'mock', + assetKind: AssetKind.NAT, + displayInfo: { assetKind: AssetKind.NAT }, + amountShape: M.any(), +}); + /** @type {Brand} */ export const mockBrand = Far('brand', { - // eslint-disable-next-line no-unused-vars - isMyIssuer: async allegedIssuer => false, - getAllegedName: () => 'mock', - getAmountShape: () => {}, - getDisplayInfo: () => ({ - assetKind: AssetKind.NAT, - }), + getAllegedName: () => mockAuxData.name, + isMyIssuer: async _allegedIssuer => false, + getDisplayInfo: () => mockAuxData.displayInfo, + getAmountShape: () => mockAuxData.amountShape, + aux: () => mockAuxData, }); diff --git a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js index 102af4db0de5..44192e3369ab 100644 --- a/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js +++ b/packages/ERTP/test/unitTests/mathHelpers/test-natMathHelpers.js @@ -10,6 +10,21 @@ import { mockBrand } from './mockBrand.js'; // AmountMath so that we can test that any duplication is handled // correctly. +const otherAuxData = harden({ + name: 'somename', + assetKind: AssetKind.NAT, + displayInfo: { assetKind: AssetKind.NAT }, + amountShape: M.any(), +}); + +const otherBrand = Far('otherBrand', { + getAllegedName: () => otherAuxData.name, + isMyIssuer: async _allegedIssuer => false, + getDisplayInfo: () => otherAuxData.displayInfo, + getAmountShape: () => otherAuxData.amountShape, + aux: () => otherAuxData, +}); + test('natMathHelpers make', t => { t.deepEqual(m.make(mockBrand, 4n), { brand: mockBrand, value: 4n }); // @ts-expect-error deliberate invalid arguments for testing @@ -62,12 +77,7 @@ test('natMathHelpers coerce', t => { m.coerce( mockBrand, harden({ - brand: Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), + brand: otherBrand, value: 4n, }), ), @@ -186,39 +196,14 @@ test('natMathHelpers isGTE', t => { }); test('natMathHelpers isGTE mixed brands', t => { - t.throws( - () => - m.isGTE( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 5n, - ), - m.make(mockBrand, 3n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.isGTE(m.make(otherBrand, 5n), m.make(mockBrand, 3n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers isGTE - brands don't match objective brand`, t => { t.throws( - () => - m.isGTE( - m.make(mockBrand, 5n), - m.make(mockBrand, 3n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.isGTE(m.make(mockBrand, 5n), m.make(mockBrand, 3n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, @@ -237,39 +222,14 @@ test('natMathHelpers isEqual', t => { }); test('natMathHelpers isEqual mixed brands', t => { - t.throws( - () => - m.isEqual( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 4n, - ), - m.make(mockBrand, 4n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.isEqual(m.make(otherBrand, 4n), m.make(mockBrand, 4n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers isEqual - brands don't match objective brand`, t => { t.throws( - () => - m.isEqual( - m.make(mockBrand, 4n), - m.make(mockBrand, 4n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.isEqual(m.make(mockBrand, 4n), m.make(mockBrand, 4n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, @@ -285,39 +245,14 @@ test('natMathHelpers add', t => { }); test('natMathHelpers add mixed brands', t => { - t.throws( - () => - m.add( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 5n, - ), - m.make(mockBrand, 9n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.add(m.make(otherBrand, 5n), m.make(mockBrand, 9n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers add - brands don't match objective brand`, t => { t.throws( - () => - m.add( - m.make(mockBrand, 5n), - m.make(mockBrand, 9n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.add(m.make(mockBrand, 5n), m.make(mockBrand, 9n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, @@ -333,39 +268,14 @@ test('natMathHelpers subtract', t => { }); test('natMathHelpers subtract mixed brands', t => { - t.throws( - () => - m.subtract( - m.make( - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - 6n, - ), - m.make(mockBrand, 1n), - ), - { - message: /Brands in left .* and right .* should match but do not/, - }, - ); + t.throws(() => m.subtract(m.make(otherBrand, 6n), m.make(mockBrand, 1n)), { + message: /Brands in left .* and right .* should match but do not/, + }); }); test(`natMathHelpers subtract brands don't match brand`, t => { t.throws( - () => - m.subtract( - m.make(mockBrand, 6n), - m.make(mockBrand, 1n), - Far('otherBrand', { - getAllegedName: () => 'somename', - isMyIssuer: async () => false, - getDisplayInfo: () => ({ assetKind: AssetKind.NAT }), - getAmountShape: () => M.any(), - }), - ), + () => m.subtract(m.make(mockBrand, 6n), m.make(mockBrand, 1n), otherBrand), { message: /amount's brand .* did not match expected brand .*/, }, diff --git a/packages/store/src/types.js b/packages/store/src/types.js index 22d4e5cacebd..6553b7c2c0a0 100644 --- a/packages/store/src/types.js +++ b/packages/store/src/types.js @@ -708,22 +708,40 @@ // TODO parameterize this to match the behavior object it guards /** * @typedef {{ - * klass: 'Interface', - * interfaceName: string, - * methodGuards: Record - * sloppy?: boolean + * klass: 'Interface', + * interfaceName: string, + * methodGuards: Record + * sloppy?: boolean * }} InterfaceGuard */ -/** @typedef {any} MethodGuardMaker +/** + * @typedef {any} MethodGuardMaker * a parameter list like foo(a, b, c = d, …e) => f should be guarded by * something like * foo: M.call(AShape, BShape).optional(CShape).rest(EShape).returns(FShape) * optional is for optional (=) params. rest is for … (varargs) params */ -/** @typedef {{ klass: 'methodGuard', callKind: 'sync' | 'async', returnGuard: unknown }} MethodGuard */ -/** @typedef {any} ArgGuard */ +/** + * @typedef {{ + * klass: 'methodGuard', + * callKind: 'sync' | 'async', + * argGuards: ArgGuard[], + * optionalArgGuards: ArgGuard[], + * restArgGuard: Pattern, + * returnGuard: Pattern + * }} MethodGuard + */ + +/** + * @typedef {{ + * klass: 'awaitArg', + * argGuard: ArgGuard, + * }} AwaitArgGuard + */ + +/** @typedef {Pattern | AwaitArgGuard } ArgGuard */ /** * @typedef {object} PatternKit diff --git a/packages/swingset-liveslots/src/liveslots.js b/packages/swingset-liveslots/src/liveslots.js index 02e3711a2494..6d62d0fe6ab5 100644 --- a/packages/swingset-liveslots/src/liveslots.js +++ b/packages/swingset-liveslots/src/liveslots.js @@ -18,11 +18,64 @@ import { makeCollectionManager } from './collectionManager.js'; import { makeWatchedPromiseManager } from './watchedPromises.js'; import { releaseOldState } from './stop-vat.js'; +/** @template T @typedef {import('@endo/far').ERef} ERef */ + +const { defineProperty } = Object; + const DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE = 3; // XXX ridiculously small value to force churn for testing const SYSCALL_CAPDATA_BODY_SIZE_LIMIT = 10_000_000; const SYSCALL_CAPDATA_SLOTS_LENGTH_LIMIT = 10_000; +// TODO Move someplace more reusable; perhaps even @endo/marshal +const makeRemotePresence = (iface, fulfilledHandler) => { + let remotePresence; + const p = new HandledPromise((_res, _rej, resolveWithPresence) => { + remotePresence = resolveWithPresence(fulfilledHandler); + let auxData; + let hasAuxData = false; + + /** + * A remote presence has an `aux()` method that either returns a promise + * or a non-promise. The first time it is called, it does an eventual-send + * of an `aux()` message to the far object it designates, remembers that + * promise as its auxdata, and returns that. It also watches that promise + * to react to its fulfillment. Until that reaction, every time its + * `aux()` is called, it will return that same promise. Once it reacts + * to the promise being fulfilled, if that ever happens, then the + * fulfillment becomes its new auxdata which gets returned from then on. + * + * @template T + * @returns {ERef} + */ + const aux = () => { + if (hasAuxData) { + return auxData; + } + auxData = E(remotePresence).aux(); + hasAuxData = true; + // TODO Also watch for a rejection. If rejected with the special + // value indicating the promise was broken by upgrade, then ask again. + // To keep the return promise valid across that upgrade-polling + // requires more bookkeeping, to keep the returned promise distinct + // from the promise for the result of the send. + E.when(auxData, value => (auxData = value)); + return auxData; + }; + defineProperty(remotePresence, 'aux', { + value: aux, + writable: false, + enumerable: false, + configurable: false, + }); + // Use Remotable rather than Far to make a remote from a presence + Remotable(iface, undefined, remotePresence); + }); + + harden(p); + return remotePresence; +}; + // 'makeLiveSlots' is a dispatcher which uses javascript Maps to keep track // of local objects which have been exported. These cannot be persisted // beyond the runtime of the javascript environment, so this mechanism is not @@ -375,22 +428,17 @@ function build( }, }; - let remotePresence; - const p = new HandledPromise((_res, _rej, resolveWithPresence) => { - // Use Remotable rather than Far to make a remote from a presence - remotePresence = Remotable( - iface, - undefined, - resolveWithPresence(fulfilledHandler), - ); - // remote === presence, actually + const remotePresence = makeRemotePresence(iface, fulfilledHandler); - // todo: mfig says resolveWithPresence - // gives us a Presence, Remotable gives us a Remote. I think that - // implies we have a lot of renaming to do, 'makeRemote' instead of - // 'makeImportedPresence', etc. I'd like to defer that for a later - // cleanup/renaming pass. - }); // no unfulfilledHandler + // remote === presence, actually + + // todo: mfig says resolveWithPresence + // gives us a Presence, Remotable gives us a Remote. I think that + // implies we have a lot of renaming to do, 'makeRemote' instead of + // 'makeImportedPresence', etc. I'd like to defer that for a later + // cleanup/renaming pass. + + // no unfulfilledHandler // The call to resolveWithPresence performs the forwarding logic // immediately, so by the time we reach here, E(presence).foo() will use @@ -402,8 +450,6 @@ function build( // `HandledPromise.resolve(presence)`. So we must harden it now, for // safety, to prevent it from being used as a communication channel // between isolated objects that share a reference to the Presence. - harden(p); - // Up at the application level, presence~.foo(args) starts by doing // HandledPromise.resolve(presence), which retrieves it, and then does // p.eventualSend('foo', [args]), which uses the fulfilledHandler. diff --git a/packages/zoe/src/issuerStorage.js b/packages/zoe/src/issuerStorage.js index 4dfb3d33d0fc..6f911dfe8b36 100644 --- a/packages/zoe/src/issuerStorage.js +++ b/packages/zoe/src/issuerStorage.js @@ -87,15 +87,16 @@ export const provideIssuerStorage = zcfBaggage => { assertInstantiated(); const brandP = E(issuerP).getBrand(); const brandIssuerMatchP = E(brandP).isMyIssuer(issuerP); - const displayInfoP = E(brandP).getDisplayInfo(); - /** @type {[Issuer,Brand,boolean,DisplayInfo]} */ - const [issuer, brand, brandIssuerMatch, displayInfo] = await Promise.all([ + const brandAuxDataP = E(brandP).aux(); + /** @type {[Issuer,Brand,boolean,BrandAuxData]} */ + const [issuer, brand, brandIssuerMatch, brandAuxData] = await Promise.all([ issuerP, brandP, brandIssuerMatchP, - displayInfoP, + brandAuxDataP, ]); // AWAIT ///// + const { displayInfo } = brandAuxData; if (issuerToIssuerRecord.has(issuer)) { return issuerToIssuerRecord.get(issuer); diff --git a/packages/zoe/test/unitTests/test-zoe.js b/packages/zoe/test/unitTests/test-zoe.js index d47ff565f0d8..d20a8e3c79cd 100644 --- a/packages/zoe/test/unitTests/test-zoe.js +++ b/packages/zoe/test/unitTests/test-zoe.js @@ -7,6 +7,7 @@ import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { passStyleOf, Far } from '@endo/marshal'; import { getMethodNames } from '@agoric/internal'; +import { M } from '@agoric/store'; // eslint-disable-next-line import/no-extraneous-dependencies import bundleSource from '@endo/bundle-source'; @@ -156,6 +157,13 @@ test(`E(zoe).startInstance - bad issuer, makeEmptyPurse throws`, async t => { // eslint-disable-next-line no-use-before-define isMyIssuer: i => i === badIssuer, getDisplayInfo: () => ({ decimalPlaces: 6, assetKind: AssetKind.NAT }), + aux: () => + harden({ + name: 'bogusBrand', + assetKind: AssetKind.NAT, + displayInfo: { decimalPlaces: 6, assetKind: AssetKind.NAT }, + amountShape: M.any(), + }), }); const badIssuer = Far('issuer', { makeEmptyPurse: async () => { diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index 11d591b5ae94..1b4f5a301d6f 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -5,8 +5,9 @@ import { Far } from '@endo/marshal'; import { AssetKind, AmountMath } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { getMethodNames } from '@agoric/internal'; -import { makeOffer } from '../makeOffer.js'; +import { M } from '@agoric/store'; +import { makeOffer } from '../makeOffer.js'; import { setup } from '../setupBasicMints.js'; import buildManualTimer from '../../../tools/manualTimer.js'; @@ -204,6 +205,13 @@ test(`zcf.saveIssuer - bad issuer, makeEmptyPurse throws`, async t => { // eslint-disable-next-line no-use-before-define isMyIssuer: i => i === badIssuer, getDisplayInfo: () => ({ decimalPlaces: 6, assetKind: AssetKind.NAT }), + aux: () => + harden({ + name: 'bogusBrand', + assetKind: AssetKind.NAT, + displayInfo: { decimalPlaces: 6, assetKind: AssetKind.NAT }, + amountShape: M.any(), + }), }); const badIssuer = Far('issuer', { makeEmptyPurse: async () => {