From d2ad04b39554d7b29dcd803265d8270f20c5d1e5 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 10 Jun 2022 13:55:54 -0700 Subject: [PATCH] refactor: Durability for ERTP --- packages/ERTP/package.json | 1 + packages/ERTP/src/brand.js | 81 ---------- packages/ERTP/src/issuerKit.js | 147 +++++++++++++----- packages/ERTP/src/payment.js | 22 ++- packages/ERTP/src/paymentLedger.js | 142 ++++++++++++----- packages/ERTP/src/purse.js | 80 +++++++--- .../bootstrap-ertp-service-upgrade.js | 102 ++++++++++++ .../ertpService/test-ertp-service-upgrade.js | 49 ++++++ .../ertpService/vat-ertp-service.js | 52 +++++++ .../test/unitTests/test-inputValidation.js | 14 +- .../ERTP/test/unitTests/test-interfaces.js | 4 +- packages/ERTP/test/unitTests/test-mintObj.js | 26 ++-- .../src/liveslots/virtualObjectManager.js | 2 +- .../test/promise-watcher/vat-upton.js | 19 +-- .../test/vo-test-harness/vat-dvo-test-test.js | 20 +-- .../contractGovernor/test-governor.js | 30 ++-- .../run-protocol/src/runStake/attestation.js | 18 +-- packages/vat-data/src/index.js | 86 +--------- packages/vat-data/src/index.test-d.ts | 8 +- packages/vat-data/src/kind-utils.js | 59 +++++++ packages/vat-data/src/types.d.ts | 19 ++- packages/vat-data/src/vat-data-bindings.js | 107 +++++++++++++ packages/vats/src/vat-zoe.js | 3 +- packages/wallet/api/test/test-lib-wallet.js | 6 +- packages/zoe/src/makeHandle.js | 23 ++- .../zoe/src/zoeService/installationStorage.js | 46 ++++-- packages/zoe/src/zoeService/startInstance.js | 13 +- packages/zoe/src/zoeService/zoe.js | 4 + .../zoe/src/zoeService/zoeStorageManager.js | 9 +- .../brokenContracts/test-crashingContract.js | 1 + .../swingsetTests/offerArgs/test-offerArgs.js | 1 + .../zoe/test/swingsetTests/zoe/test-zoe.js | 1 + 32 files changed, 819 insertions(+), 376 deletions(-) delete mode 100644 packages/ERTP/src/brand.js create mode 100644 packages/ERTP/test/swingsetTests/ertpService/bootstrap-ertp-service-upgrade.js create mode 100644 packages/ERTP/test/swingsetTests/ertpService/test-ertp-service-upgrade.js create mode 100644 packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js create mode 100644 packages/vat-data/src/kind-utils.js create mode 100644 packages/vat-data/src/vat-data-bindings.js diff --git a/packages/ERTP/package.json b/packages/ERTP/package.json index b3b66ceeb9c1..f3e72afa4587 100644 --- a/packages/ERTP/package.json +++ b/packages/ERTP/package.json @@ -43,6 +43,7 @@ "@agoric/swingset-vat": "^0.28.0", "@agoric/vat-data": "^0.3.1", "@endo/eventual-send": "^0.15.5", + "@endo/far": "^0.2.5", "@endo/marshal": "^0.6.9", "@endo/promise-kit": "^0.2.43" }, diff --git a/packages/ERTP/src/brand.js b/packages/ERTP/src/brand.js deleted file mode 100644 index af90e6b53e62..000000000000 --- a/packages/ERTP/src/brand.js +++ /dev/null @@ -1,81 +0,0 @@ -// @ts-check -import { M } from '@agoric/store'; -import { E } from '@endo/eventual-send'; -import { Far } from '@endo/marshal'; - -const { details: X, quote: q } = assert; - -/** - * @param {string} allegedName - * @param {(allegedIssuer: Issuer) => boolean} isMyIssuerNow - * @param {DisplayInfo} displayInfo - * @param {AssetKind} assetKind - * @param {Pattern} elementSchema - * @returns {Brand} - */ -export const makeBrand = ( - allegedName, - isMyIssuerNow, - displayInfo, - assetKind, - elementSchema, -) => { - /** @type {Brand} */ - const brand = Far(`${allegedName} brand`, { - isMyIssuer: allegedIssuerP => E.when(allegedIssuerP, isMyIssuerNow), - - getAllegedName: () => allegedName, - - // Give information to UI on how to display the amount. - getDisplayInfo: () => displayInfo, - - // eslint-disable-next-line no-use-before-define - getAmountSchema: () => amountSchema, - }); - - let valueSchema; - switch (assetKind) { - case 'nat': { - valueSchema = M.nat(); - assert( - elementSchema === undefined, - X`Fungible assets cannot have an elementSchema: ${q(elementSchema)}`, - ); - break; - } - case 'set': { - if (elementSchema === undefined) { - valueSchema = M.arrayOf(M.key()); - } else { - valueSchema = M.arrayOf(M.and(M.key(), elementSchema)); - } - break; - } - case 'copySet': { - if (elementSchema === undefined) { - valueSchema = M.set(); - } else { - valueSchema = M.setOf(elementSchema); - } - break; - } - case 'copyBag': { - if (elementSchema === undefined) { - valueSchema = M.bag(); - } else { - valueSchema = M.bagOf(elementSchema); - } - break; - } - default: { - assert.fail(X`unexpected asset kind ${q(assetKind)}`); - } - } - - const amountSchema = harden({ - brand, // matches only this exact brand - value: valueSchema, - }); - - return brand; -}; diff --git a/packages/ERTP/src/issuerKit.js b/packages/ERTP/src/issuerKit.js index 160495342d0b..40a3a90ff76e 100644 --- a/packages/ERTP/src/issuerKit.js +++ b/packages/ERTP/src/issuerKit.js @@ -3,11 +3,11 @@ import { assert } from '@agoric/assert'; import { assertPattern } from '@agoric/store'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; import { AssetKind, assertAssetKind } from './amountMath.js'; import { coerceDisplayInfo } from './displayInfo.js'; -import { makeBrand } from './brand.js'; -import { makePaymentLedger } from './paymentLedger.js'; +import { makeDurablePaymentLedger } from './paymentLedger.js'; import './types.js'; @@ -24,9 +24,7 @@ import './types.js'; * * `displayInfo` gives information to the UI on how to display the amount. * - * @param {string} allegedName - * @param {K} [assetKind=AssetKind.NAT] - * @param {AdditionalDisplayInfo} [displayInfo={}] + * @param {MapStore} issuerBaggage * @param {ShutdownWithFailure=} optShutdownWithFailure If this issuer fails * in the middle of an atomic action (which btw should never happen), it * potentially leaves its ledger in a corrupted state. If this function was @@ -34,7 +32,6 @@ import './types.js'; * larger unit of computation, like the enclosing vat, can be shutdown * before anything else is corrupted by that corrupted state. * See https://github.com/Agoric/agoric-sdk/issues/3434 - * @param {Partial<{elementSchema: Pattern}>} [options] * @returns {{ * mint: Mint, * issuer: Issuer, @@ -42,14 +39,14 @@ import './types.js'; * displayInfo: DisplayInfo, * }} */ -const makeIssuerKit = ( - allegedName, - // @ts-expect-error K could be instantiated with a different subtype of AssetKind - assetKind = AssetKind.NAT, - displayInfo = harden({}), +export const provideDurableIssuerKit = ( + issuerBaggage, optShutdownWithFailure = undefined, - { elementSchema = undefined } = {}, ) => { + const allegedName = issuerBaggage.get('allegedName'); + const assetKind = issuerBaggage.get('assetKind'); + const displayInfo = issuerBaggage.get('displayInfo'); + const elementSchema = issuerBaggage.get('elementSchema'); assert.typeof(allegedName, 'string'); assertAssetKind(assetKind); @@ -63,32 +60,13 @@ const makeIssuerKit = ( assertPattern(elementSchema); } - /** - * We can define this function to use the in-scope `issuer` variable - * before that variable is initialized, as long as the variable is - * initialized before the function is called. - * - * @param {Issuer} allegedIssuer - * @returns {boolean} - */ - // eslint-disable-next-line no-use-before-define - const isMyIssuerNow = allegedIssuer => allegedIssuer === issuer; - - const brand = makeBrand( - allegedName, - isMyIssuerNow, - cleanDisplayInfo, - assetKind, - elementSchema, - ); - // Attenuate the powerful authority to mint and change balances - const { issuer, mint } = makePaymentLedger( + const { issuer, mint, brand } = makeDurablePaymentLedger( + issuerBaggage, allegedName, - brand, assetKind, cleanDisplayInfo, - brand.getAmountSchema(), + elementSchema, optShutdownWithFailure, ); @@ -99,9 +77,104 @@ const makeIssuerKit = ( displayInfo: cleanDisplayInfo, }); }; +harden(provideDurableIssuerKit); -harden(makeIssuerKit); +/** @typedef {ReturnType} IssuerKit */ -export { makeIssuerKit }; +/** + * @template {AssetKind} K + * The allegedName becomes part of the brand in asset descriptions. The + * allegedName doesn't have to be a string, but it will only be used for + * its value. The allegedName is useful for debugging and double-checking + * assumptions, but should not be trusted. + * + * The assetKind will be used to import a specific mathHelpers + * from the mathHelpers library. For example, natMathHelpers, the + * default, is used for basic fungible tokens. + * + * `displayInfo` gives information to the UI on how to display the amount. + * + * @param {MapStore} issuerBaggage + * @param {string} allegedName + * @param {K} [assetKind=AssetKind.NAT] + * @param {AdditionalDisplayInfo} [displayInfo={}] + * @param {ShutdownWithFailure=} optShutdownWithFailure If this issuer fails + * in the middle of an atomic action (which btw should never happen), it + * potentially leaves its ledger in a corrupted state. If this function was + * provided, then the failed atomic action will call it, so that some + * larger unit of computation, like the enclosing vat, can be shutdown + * before anything else is corrupted by that corrupted state. + * See https://github.com/Agoric/agoric-sdk/issues/3434 + * @param {Partial<{elementSchema: Pattern}>} [options] + * @returns {{ + * mint: Mint, + * issuer: Issuer, + * brand: Brand, + * displayInfo: DisplayInfo, + * }} + */ +export const makeDurableIssuerKit = ( + issuerBaggage, + allegedName, + // @ts-expect-error K could be instantiated with a different subtype of AssetKind + assetKind = AssetKind.NAT, + displayInfo = harden({}), + optShutdownWithFailure = undefined, + { elementSchema = undefined } = {}, +) => { + issuerBaggage.init('allegedName', allegedName); + issuerBaggage.init('assetKind', assetKind); + issuerBaggage.init('displayInfo', displayInfo); + issuerBaggage.init('elementSchema', elementSchema); + return provideDurableIssuerKit(issuerBaggage, optShutdownWithFailure); +}; +harden(makeDurableIssuerKit); -/** @typedef {ReturnType} IssuerKit */ +/** + * @template {AssetKind} K + * The allegedName becomes part of the brand in asset descriptions. The + * allegedName doesn't have to be a string, but it will only be used for + * its value. The allegedName is useful for debugging and double-checking + * assumptions, but should not be trusted. + * + * The assetKind will be used to import a specific mathHelpers + * from the mathHelpers library. For example, natMathHelpers, the + * default, is used for basic fungible tokens. + * + * `displayInfo` gives information to the UI on how to display the amount. + * + * @param {string} allegedName + * @param {K} [assetKind=AssetKind.NAT] + * @param {AdditionalDisplayInfo} [displayInfo={}] + * @param {ShutdownWithFailure=} optShutdownWithFailure If this issuer fails + * in the middle of an atomic action (which btw should never happen), it + * potentially leaves its ledger in a corrupted state. If this function was + * provided, then the failed atomic action will call it, so that some + * larger unit of computation, like the enclosing vat, can be shutdown + * before anything else is corrupted by that corrupted state. + * See https://github.com/Agoric/agoric-sdk/issues/3434 + * @param {Partial<{elementSchema: Pattern}>} [options] + * @returns {{ + * mint: Mint, + * issuer: Issuer, + * brand: Brand, + * displayInfo: DisplayInfo, + * }} + */ +export const makeIssuerKit = ( + allegedName, + // @ts-expect-error K could be instantiated with a different subtype of AssetKind + assetKind = AssetKind.NAT, + displayInfo = harden({}), + optShutdownWithFailure = undefined, + { elementSchema = undefined } = {}, +) => + makeDurableIssuerKit( + makeScalarBigMapStore('dropped issuer kit', { durable: true }), + allegedName, + assetKind, + displayInfo, + optShutdownWithFailure, + { elementSchema }, + ); +harden(makeIssuerKit); diff --git a/packages/ERTP/src/payment.js b/packages/ERTP/src/payment.js index c8fc9cbe3303..69ec95e8df70 100644 --- a/packages/ERTP/src/payment.js +++ b/packages/ERTP/src/payment.js @@ -1,17 +1,27 @@ // @ts-check -import { defineKind } from '@agoric/vat-data'; +import { defineDurableKind, provideKindHandle } from '@agoric/vat-data'; /** * @template {AssetKind} K + * @param {MapStore} issuerBaggage * @param {string} allegedName - * @param {Brand} brand + * @param {() => Brand} getBrand must not be called before the issuerKit is + * created * @returns {() => Payment} */ -export const definePaymentKind = (allegedName, brand) => { - const makePayment = defineKind(`${allegedName} payment`, () => ({}), { - getAllegedBrand: () => brand, +export const defineDurablePaymentKind = ( + issuerBaggage, + allegedName, + getBrand, +) => { + const paymentKindHandle = provideKindHandle( + issuerBaggage, + `${allegedName} payment`, + ); + const makePayment = defineDurableKind(paymentKindHandle, () => ({}), { + getAllegedBrand: getBrand, }); return makePayment; }; -harden(definePaymentKind); +harden(defineDurablePaymentKind); diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index 69c66fa6ca3c..f2ad626c54b4 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -1,40 +1,94 @@ +/* eslint-disable no-use-before-define */ // @ts-check import { E } from '@endo/eventual-send'; import { isPromise } from '@endo/promise-kit'; -import { Far, assertCopyArray } from '@endo/marshal'; -import { fit } from '@agoric/store'; -import { makeScalarBigWeakMapStore } from '@agoric/vat-data'; +import { assertCopyArray } from '@endo/marshal'; +import { fit, M } from '@agoric/store'; +import { ProvideFar, provideDurableWeakMapStore } from '@agoric/vat-data'; import { AmountMath } from './amountMath.js'; -import { definePaymentKind } from './payment.js'; -import { makePurseMaker } from './purse.js'; +import { defineDurablePaymentKind } from './payment.js'; +import { defineDurablePurse } from './purse.js'; import '@agoric/store/exported.js'; -const { details: X } = assert; +const { details: X, quote: q } = assert; + +const amountSchemaFromElementSchema = (brand, assetKind, elementSchema) => { + let valueSchema; + switch (assetKind) { + case 'nat': { + valueSchema = M.nat(); + assert( + elementSchema === undefined, + X`Fungible assets cannot have an elementSchema: ${q(elementSchema)}`, + ); + break; + } + case 'set': { + if (elementSchema === undefined) { + valueSchema = M.arrayOf(M.key()); + } else { + valueSchema = M.arrayOf(M.and(M.key(), elementSchema)); + } + break; + } + case 'copySet': { + if (elementSchema === undefined) { + valueSchema = M.set(); + } else { + valueSchema = M.setOf(elementSchema); + } + break; + } + case 'copyBag': { + if (elementSchema === undefined) { + valueSchema = M.bag(); + } else { + valueSchema = M.bagOf(elementSchema); + } + break; + } + default: { + assert.fail(X`unexpected asset kind ${q(assetKind)}`); + } + } + + const amountSchema = harden({ + brand, // matches only this exact brand + value: valueSchema, + }); + return amountSchema; +}; /** * Make the paymentLedger, the source of truth for the balances of * payments. All minting and transfer authority originates here. * * @template {AssetKind} [K=AssetKind] + * @param {MapStore} issuerBaggage * @param {string} allegedName - * @param {Brand} brand * @param {AssetKind} assetKind * @param {DisplayInfo} displayInfo - * @param {Pattern} amountSchema + * @param {Pattern} elementSchema * @param {ShutdownWithFailure=} optShutdownWithFailure - * @returns {{ issuer: Issuer, mint: Mint }} + * @returns {{ issuer: Issuer, mint: Mint, brand: Brand }} */ -export const makePaymentLedger = ( +export const makeDurablePaymentLedger = ( + issuerBaggage, allegedName, - brand, assetKind, displayInfo, - amountSchema, + elementSchema, optShutdownWithFailure = undefined, ) => { - const makePayment = definePaymentKind(allegedName, brand); + const getBrand = () => brand; + + const makePayment = defineDurablePaymentKind( + issuerBaggage, + allegedName, + getBrand, + ); /** @type {ShutdownWithFailure} */ const shutdownLedgerWithFailure = reason => { @@ -52,7 +106,10 @@ export const makePaymentLedger = ( }; /** @type {WeakMapStore} */ - const paymentLedger = makeScalarBigWeakMapStore('payment'); + const paymentLedger = provideDurableWeakMapStore( + issuerBaggage, + 'paymentLedger', + ); /** * A withdrawn live payment is associated with the recovery set of @@ -74,7 +131,10 @@ export const makePaymentLedger = ( * * @type {WeakMapStore>} */ - const paymentRecoverySets = makeScalarBigWeakMapStore('payment-recovery'); + const paymentRecoverySets = provideDurableWeakMapStore( + issuerBaggage, + 'paymentRecoverySets', + ); /** * To maintain the invariants listed in the `paymentRecoverySets` comment, @@ -118,9 +178,6 @@ export const makePaymentLedger = ( /** @type {(left: Amount, right: Amount) => boolean } */ const isEqual = (left, right) => AmountMath.isEqual(left, right, brand); - /** @type {Amount} */ - const emptyAmount = AmountMath.makeEmpty(brand, assetKind); - /** * Methods like deposit() have an optional second parameter * `optAmountShape` @@ -399,20 +456,18 @@ export const makePaymentLedger = ( return payment; }; - const purseMethods = { - depositInternal, - withdrawInternal, - }; - - const makeEmptyPurse = makePurseMaker( + const makeEmptyPurse = defineDurablePurse( + issuerBaggage, allegedName, assetKind, - brand, - purseMethods, + getBrand, + harden({ + depositInternal, + withdrawInternal, + }), ); - /** @type {Issuer} */ - const issuer = Far(`${allegedName} issuer`, { + const issuer = ProvideFar(issuerBaggage, `${allegedName} issuer`, { isLive, getAmountOf, burn, @@ -427,16 +482,33 @@ export const makePaymentLedger = ( makeEmptyPurse, }); - /** @type {Mint} */ - const mint = Far(`${allegedName} mint`, { + const mint = ProvideFar(issuerBaggage, `${allegedName} mint`, { getIssuer: () => issuer, mintPayment, }); - return harden({ - issuer, - mint, - }); + const brand = /** @type {Brand} */ ( + ProvideFar(issuerBaggage, `${allegedName} brand`, { + isMyIssuer: allegedIssuerP => + E.when(allegedIssuerP, allegedIssuer => allegedIssuer === issuer), + + getAllegedName: () => allegedName, + // Give information to UI on how to display the amount. + getDisplayInfo: () => displayInfo, + getAmountSchema: () => amountSchema, + }) + ); + + const issuerKit = harden({ issuer, mint, brand }); + + const emptyAmount = AmountMath.makeEmpty(brand, assetKind); + const amountSchema = amountSchemaFromElementSchema( + brand, + assetKind, + elementSchema, + ); + return issuerKit; }; +harden(makeDurablePaymentLedger); -/** @typedef {ReturnType} PaymentLedger */ +/** @typedef {ReturnType} PaymentLedger */ diff --git a/packages/ERTP/src/purse.js b/packages/ERTP/src/purse.js index f35e6e0facd4..d0dc86d9d581 100644 --- a/packages/ERTP/src/purse.js +++ b/packages/ERTP/src/purse.js @@ -1,13 +1,46 @@ +import { + defineDurableKindMulti, + makeScalarBigSetStore, + provideKindHandle, + makeScalarBigWeakMapStore, +} from '@agoric/vat-data'; +import { provide } from '@agoric/store'; import { makeNotifierKit } from '@agoric/notifier'; -import { defineKindMulti, makeScalarBigSetStore } from '@agoric/vat-data'; import { AmountMath } from './amountMath.js'; const { details: X } = assert; -export const makePurseMaker = (allegedName, assetKind, brand, purseMethods) => { - const updatePurseBalance = (state, newPurseBalance) => { +// `getBrand` must not be called before the issuerKit is created +export const defineDurablePurse = ( + issuerBaggage, + name, + assetKind, + getBrand, + purseMethods, +) => { + // Note: Virtual for high cardinality, but *not* durable, and so + // broken across an upgrade. + /** @type {WeakMapStore>} */ + const transientNotiferKits = makeScalarBigWeakMapStore( + 'transientNotiferKits', + ); + + const provideNotifierKit = purse => + provide(transientNotiferKits, purse, () => + makeNotifierKit(purse.getCurrentAmount()), + ); + + const provideNotifier = purse => provideNotifierKit(purse).notifier; + const notifyBalance = purse => { + if (transientNotiferKits.has(purse)) { + const { updater } = transientNotiferKits.get(purse); + updater.updateState(purse.getCurrentAmount()); + } + }; + + const updatePurseBalance = (state, newPurseBalance, purse) => { state.currentBalance = newPurseBalance; - state.balanceUpdater.updateState(state.currentBalance); + notifyBalance(purse); }; // - This kind is a pair of purse and depositFacet that have a 1:1 @@ -17,53 +50,58 @@ export const makePurseMaker = (allegedName, assetKind, brand, purseMethods) => { // that created depositFacet as needed. But this approach ensures a constant // identity for the facet and exercises the multi-faceted object style. const { depositInternal, withdrawInternal } = purseMethods; - const makePurseKit = defineKindMulti( - allegedName, + const purseKitKindHandle = provideKindHandle(issuerBaggage, `${name} Purse`); + const makePurseKit = defineDurableKindMulti( + purseKitKindHandle, () => { - const currentBalance = AmountMath.makeEmpty(brand, assetKind); - - /** @type {NotifierRecord} */ - const { notifier: balanceNotifier, updater: balanceUpdater } = - makeNotifierKit(currentBalance); + const currentBalance = AmountMath.makeEmpty(getBrand(), assetKind); /** @type {SetStore} */ - const recoverySet = makeScalarBigSetStore('recovery set'); + const recoverySet = makeScalarBigSetStore('recovery set', { + durable: true, + }); return { currentBalance, - balanceNotifier, - balanceUpdater, recoverySet, }; }, { purse: { - deposit: ({ state }, srcPayment, optAmountShape = undefined) => { + deposit: ( + { state, facets: { purse } }, + srcPayment, + optAmountShape = undefined, + ) => { // Note COMMIT POINT within deposit. return depositInternal( state.currentBalance, - newPurseBalance => updatePurseBalance(state, newPurseBalance), + newPurseBalance => + updatePurseBalance(state, newPurseBalance, purse), srcPayment, optAmountShape, state.recoverySet, ); }, - withdraw: ({ state }, amount) => + withdraw: ({ state, facets: { purse } }, amount) => // Note COMMIT POINT within withdraw. withdrawInternal( state.currentBalance, - newPurseBalance => updatePurseBalance(state, newPurseBalance), + newPurseBalance => + updatePurseBalance(state, newPurseBalance, purse), amount, state.recoverySet, ), getCurrentAmount: ({ state }) => state.currentBalance, - getCurrentAmountNotifier: ({ state }) => state.balanceNotifier, - getAllegedBrand: () => brand, + getCurrentAmountNotifier: ({ facets: { purse } }) => + provideNotifier(purse), + getAllegedBrand: _context => getBrand(), // eslint-disable-next-line no-use-before-define getDepositFacet: ({ facets }) => facets.depositFacet, getRecoverySet: ({ state }) => state.recoverySet.snapshot(), recoverAll: ({ state, facets }) => { + const brand = getBrand(); let amount = AmountMath.makeEmpty(brand, assetKind); for (const payment of state.recoverySet.keys()) { // This does cause deletions from the set while iterating, @@ -85,4 +123,4 @@ export const makePurseMaker = (allegedName, assetKind, brand, purseMethods) => { ); return () => makePurseKit().purse; }; -harden(makePurseMaker); +harden(defineDurablePurse); diff --git a/packages/ERTP/test/swingsetTests/ertpService/bootstrap-ertp-service-upgrade.js b/packages/ERTP/test/swingsetTests/ertpService/bootstrap-ertp-service-upgrade.js new file mode 100644 index 000000000000..1403a01b49d7 --- /dev/null +++ b/packages/ERTP/test/swingsetTests/ertpService/bootstrap-ertp-service-upgrade.js @@ -0,0 +1,102 @@ +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; +import { AmountMath } from '../../../src'; + +const mintInto = (kit, purse, value) => + E(kit.mint) + .mintPayment(AmountMath.make(kit.brand, value)) + .then(p => E(purse).deposit(p)); + +export const buildRootObject = () => { + let vatAdmin; + let ertpRoot; + let ertpAdmin; + let issuerKitA; + let issuerKitB; + let purseA1; + let purseA2; + let purseB1; + let purseB2; + let paymentForBP; + + return Far('root', { + bootstrap: async (vats, devices) => { + vatAdmin = await E(vats.vatAdmin).createVatAdminService(devices.vatAdmin); + }, + + buildV1: async () => { + // build the contract vat from ZCF and the contract bundlecap + const bcap = await E(vatAdmin).getNamedBundleCap('ertpService'); + const res = await E(vatAdmin).createVat(bcap); + ertpRoot = res.root; + ertpAdmin = res.adminNode; + const ertpService = await E(ertpRoot).getErtpService(); + + issuerKitA = await E(ertpService).makeIssuerKit('A'); + issuerKitB = await E(ertpService).makeIssuerKit('B'); + + // make purses with non-zero balances + purseA1 = E(issuerKitA.issuer).makeEmptyPurse(); + purseA2 = E(issuerKitA.issuer).makeEmptyPurse(); + purseB1 = E(issuerKitB.issuer).makeEmptyPurse(); + purseB2 = E(issuerKitB.issuer).makeEmptyPurse(); + await Promise.all([ + mintInto(issuerKitA, purseA1, 20n), + mintInto(issuerKitA, purseA2, 40n), + mintInto(issuerKitB, purseB1, 12n), + mintInto(issuerKitB, purseB2, 16n), + ]); + + // hold onto a payment here. + paymentForBP = E(issuerKitB.mint).mintPayment( + AmountMath.make(issuerKitB.brand, 100n), + ); + + const purseA2Amount = await E(purseA2).getCurrentAmount(); + assert( + AmountMath.isEqual( + purseA2Amount, + AmountMath.make(issuerKitA.brand, 40n), + ), + ); + return true; + }, + + upgradeV2: async () => { + const bcap = await E(vatAdmin).getNamedBundleCap('ertpService'); + await E(ertpAdmin).upgrade(bcap); + + // exercise purses + const purseA2Balance = await E(purseA2).getCurrentAmount(); + assert( + AmountMath.isEqual( + purseA2Balance, + AmountMath.make(issuerKitA.brand, 40n), + ), + ); + const paymentA0 = await E(purseA2).withdraw( + AmountMath.make(issuerKitA.brand, 5n), + ); + const payment0Amount = await E(issuerKitA.issuer).getAmountOf(paymentA0); + assert( + AmountMath.isEqual( + payment0Amount, + AmountMath.make(issuerKitA.brand, 5n), + ), + ); + + const paymentForB = await paymentForBP; + // deposit a payment from earlier + await E(purseB1).deposit(paymentForB); + const purseB1NewAmount = await E(purseB1).getCurrentAmount(); + assert( + AmountMath.isEqual( + purseB1NewAmount, + AmountMath.make(issuerKitB.brand, 112n), + ), + ); + + return true; + }, + }); +}; diff --git a/packages/ERTP/test/swingsetTests/ertpService/test-ertp-service-upgrade.js b/packages/ERTP/test/swingsetTests/ertpService/test-ertp-service-upgrade.js new file mode 100644 index 000000000000..4d80ac23f099 --- /dev/null +++ b/packages/ERTP/test/swingsetTests/ertpService/test-ertp-service-upgrade.js @@ -0,0 +1,49 @@ +// eslint-disable-next-line import/order +import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { assert } from '@agoric/assert'; +import { buildVatController } from '@agoric/swingset-vat'; + +const bfile = name => new URL(name, import.meta.url).pathname; + +test('ertp service upgrade', async t => { + const config = { + // includeDevDependencies: true, // for vat-data + defaultManagerType: 'local', // 'xs-worker', + bootstrap: 'bootstrap', + // defaultReapInterval: 'never', + // defaultReapInterval: 1, + vats: { + bootstrap: { sourceSpec: bfile('bootstrap-ertp-service-upgrade.js') }, + }, + bundles: { + ertpService: { sourceSpec: bfile('vat-ertp-service.js') }, + }, + }; + + const c = await buildVatController(config); + c.pinVatRoot('bootstrap'); + await c.run(); + + const run = async (name, args = []) => { + assert(Array.isArray(args)); + const kpid = c.queueToVatRoot('bootstrap', name, args); + await c.run(); + const status = c.kpStatus(kpid); + console.log(`${status}`); + const capdata = c.kpResolution(kpid); + return [status, capdata]; + }; + + // create initial version + const [v1status] = await run('buildV1', []); + t.is(v1status, 'fulfilled'); + + // now perform the upgrade + console.log(`-- starting upgradeV2`); + + const [v2status] = await run('upgradeV2', []); + + t.is(v2status, 'fulfilled'); +}); diff --git a/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js b/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js new file mode 100644 index 000000000000..c5d8a08d5797 --- /dev/null +++ b/packages/ERTP/test/swingsetTests/ertpService/vat-ertp-service.js @@ -0,0 +1,52 @@ +// @ts-check + +import { Far } from '@endo/marshal'; +import { + makeScalarBigMapStore, + ProvideFar, + provideDurableSetStore, +} from '@agoric/vat-data'; + +import { + AssetKind, + makeDurableIssuerKit, + provideDurableIssuerKit, +} from '../../../src'; + +function provideErtpService(baggage, exitVatWithFailure) { + const issuerBaggageSet = provideDurableSetStore(baggage, 'BaggageSet'); + const ertpService = ProvideFar(baggage, 'ERTPService', { + makeIssuerKit: ( + allegedName, + assetKind = AssetKind.NAT, + displayInfo = harden({}), + ) => { + const issuerBaggage = makeScalarBigMapStore('IssuerBaggage', { + durable: true, + }); + const issuerKit = makeDurableIssuerKit( + issuerBaggage, + allegedName, + assetKind, + displayInfo, + exitVatWithFailure, + ); + issuerBaggageSet.add(issuerBaggage); + + return issuerKit; + }, + }); + + for (const issuerBaggage of issuerBaggageSet.values()) { + provideDurableIssuerKit(issuerBaggage); + } + + return ertpService; +} + +export const buildRootObject = async (vatPowers, _vatParams, baggage) => { + const ertpService = provideErtpService(baggage, vatPowers.exitVatWithFailure); + return Far('root', { + getErtpService: () => ertpService, + }); +}; diff --git a/packages/ERTP/test/unitTests/test-inputValidation.js b/packages/ERTP/test/unitTests/test-inputValidation.js index 34532daa0eb8..ab30fd294ee4 100644 --- a/packages/ERTP/test/unitTests/test-inputValidation.js +++ b/packages/ERTP/test/unitTests/test-inputValidation.js @@ -8,7 +8,11 @@ import { AssetKind, makeIssuerKit, AmountMath } from '../../src/index.js'; test('makeIssuerKit bad allegedName', async t => { // @ts-expect-error Intentional wrong type for testing - t.throws(() => makeIssuerKit({}), { message: `{} must be a string` }); + t.throws(() => makeIssuerKit(harden({})), { message: `{} must be a string` }); + // @ts-expect-error Intentional wrong type for testing + t.throws(() => makeIssuerKit({}), { + message: `Cannot pass non-frozen objects like {}. Use harden()`, + }); }); test('makeIssuerKit bad assetKind', async t => { @@ -107,11 +111,11 @@ test('makeIssuerKit malicious displayInfo', async t => { 'myTokens', AssetKind.NAT, // @ts-expect-error Intentional wrong type for testing - Far('malicious', { doesSomething: () => {} }), + 'bad displayInfo', ), { message: - '"displayInfo" "[Alleged: malicious]" must be a pass-by-copy record, not "remotable"', + '"displayInfo" "bad displayInfo" must be a pass-by-copy record, not "string"', }, ); }); @@ -131,6 +135,10 @@ test('makeIssuerKit bad optShutdownWithFailure', async t => { test('brand.isMyIssuer bad issuer', async t => { const { brand } = makeIssuerKit('myTokens'); // @ts-expect-error Intentional wrong type for testing + // but for some reason it is no longer complaining about the string + // being the wrong type. Hovering over `isMyIssuer` in vscode does + // show the correct type for the parameter. + // TODO(MSM): ask a typing person const result = await brand.isMyIssuer('not an issuer'); t.false(result); }); diff --git a/packages/ERTP/test/unitTests/test-interfaces.js b/packages/ERTP/test/unitTests/test-interfaces.js index b3c6f0475580..b2c11d6fcf4e 100644 --- a/packages/ERTP/test/unitTests/test-interfaces.js +++ b/packages/ERTP/test/unitTests/test-interfaces.js @@ -12,9 +12,9 @@ test('interfaces - particular implementation', t => { t.is(getInterfaceOf(brand), 'Alleged: bucks brand'); t.is(getInterfaceOf(mint), 'Alleged: bucks mint'); const purse = issuer.makeEmptyPurse(); - t.is(getInterfaceOf(purse), 'Alleged: bucks purse'); + t.is(getInterfaceOf(purse), 'Alleged: bucks Purse purse'); const depositFacet = purse.getDepositFacet(); - t.is(getInterfaceOf(depositFacet), 'Alleged: bucks depositFacet'); + t.is(getInterfaceOf(depositFacet), 'Alleged: bucks Purse depositFacet'); const payment = mint.mintPayment(AmountMath.make(brand, 2n)); t.is(getInterfaceOf(payment), 'Alleged: bucks payment'); }); diff --git a/packages/ERTP/test/unitTests/test-mintObj.js b/packages/ERTP/test/unitTests/test-mintObj.js index cc3dcb992b66..ff6325eb8b41 100644 --- a/packages/ERTP/test/unitTests/test-mintObj.js +++ b/packages/ERTP/test/unitTests/test-mintObj.js @@ -1,9 +1,11 @@ // @ts-check import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; -import { Far } from '@endo/marshal'; import { M } from '@agoric/store'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { assert } from '@agoric/assert'; +import { defineDurableKind, makeKindHandle } from '@agoric/vat-data'; import { makeIssuerKit, AssetKind, AmountMath } from '../../src/index.js'; test('mint.getIssuer', t => { @@ -47,11 +49,17 @@ test('mint.mintPayment set w strings AssetKind', async t => { }); }); +const makeDurableHandle = name => { + const kindHandle = makeKindHandle(name); + const maker = defineDurableKind(kindHandle, () => ({}), {}); + return maker(); +}; + test('mint.mintPayment set AssetKind', async t => { const { mint, issuer, brand } = makeIssuerKit('items', AssetKind.SET); - const item1handle = Far('iface', {}); - const item2handle = Far('iface', {}); - const item3handle = Far('iface', {}); + const item1handle = makeDurableHandle('iface'); + const item2handle = makeDurableHandle('iface'); + const item3handle = makeDurableHandle('iface'); const items1and2 = AmountMath.make(brand, harden([item1handle, item2handle])); const payment1 = mint.mintPayment(items1and2); const paymentBalance1 = await issuer.getAmountOf(payment1); @@ -71,18 +79,18 @@ test('mint.mintPayment set AssetKind', async t => { test('mint.mintPayment set AssetKind with invites', async t => { const { mint, issuer, brand } = makeIssuerKit('items', AssetKind.SET); - const instanceHandle1 = Far('iface', {}); + const instanceHandle1 = makeDurableHandle('iface'); const invite1Value = { - handle: Far('iface', {}), + handle: makeDurableHandle('iface'), instanceHandle: instanceHandle1, }; const invite2Value = { - handle: Far('iface', {}), + handle: makeDurableHandle('iface'), instanceHandle: instanceHandle1, }; const invite3Value = { - handle: Far('iface', {}), - instanceHandle: Far('iface', {}), + handle: makeDurableHandle('iface'), + instanceHandle: makeDurableHandle('iface'), }; const invites1and2 = AmountMath.make( brand, diff --git a/packages/SwingSet/src/liveslots/virtualObjectManager.js b/packages/SwingSet/src/liveslots/virtualObjectManager.js index ce0fd3291bf7..226bd575e24a 100644 --- a/packages/SwingSet/src/liveslots/virtualObjectManager.js +++ b/packages/SwingSet/src/liveslots/virtualObjectManager.js @@ -889,7 +889,7 @@ export function makeVirtualObjectManager( } if (missing.length) { const tags = missing.join(','); - throw Error(`defineDurableKind not called for tags: ${tags}`); + throw Error(`defineDurableKind not called for tags: [${tags}]`); } } diff --git a/packages/SwingSet/test/promise-watcher/vat-upton.js b/packages/SwingSet/test/promise-watcher/vat-upton.js index 6e3b9d7dcf05..1c598302db41 100644 --- a/packages/SwingSet/test/promise-watcher/vat-upton.js +++ b/packages/SwingSet/test/promise-watcher/vat-upton.js @@ -2,29 +2,16 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; import { - makeKindHandle, + provideKindHandle, providePromiseWatcher, defineDurableKindMulti, watchPromise, } from '@agoric/vat-data'; -const provideStoredObj = (key, mapStore, makeObj) => { - if (mapStore.has(key)) { - return mapStore.get(key); - } else { - const obj = makeObj(); - mapStore.init(key, obj); - return obj; - } -}; - -const provideKindHandle = (tag, mapStore) => - provideStoredObj(tag, mapStore, () => makeKindHandle(tag)); - export function buildRootObject(vatPowers, vatParameters, baggage) { const log = vatPowers.testLog; - const pwHandle = provideKindHandle('pwhandle', baggage); - const dkHandle = provideKindHandle('dkhandle', baggage); + const pwHandle = provideKindHandle(baggage, 'pwhandle'); + const dkHandle = provideKindHandle(baggage, 'dkhandle'); // prettier-ignore const pw = providePromiseWatcher( diff --git a/packages/SwingSet/test/vo-test-harness/vat-dvo-test-test.js b/packages/SwingSet/test/vo-test-harness/vat-dvo-test-test.js index 49fce412be6e..390436b8a92e 100644 --- a/packages/SwingSet/test/vo-test-harness/vat-dvo-test-test.js +++ b/packages/SwingSet/test/vo-test-harness/vat-dvo-test-test.js @@ -1,31 +1,19 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -import { makeKindHandle, defineDurableKind } from '@agoric/vat-data'; - -const provideStoredObj = (key, mapStore, makeObj) => { - if (mapStore.has(key)) { - return mapStore.get(key); - } else { - const obj = makeObj(); - mapStore.init(key, obj); - return obj; - } -}; - -const provideKindHandle = (tag, mapStore) => - provideStoredObj(tag, mapStore, () => makeKindHandle(tag)); +import { provide } from '@agoric/store'; +import { provideKindHandle, defineDurableKind } from '@agoric/vat-data'; export function buildRootObject(_vatPowers, vatParameters, baggage) { let other; const log = message => E(other).log(message); const testComplete = () => E(other).testComplete(); - const kh = provideKindHandle('testkind', baggage); + const kh = provideKindHandle(baggage, 'testkind'); const makeThing = defineDurableKind(kh, tag => ({ tag }), { getTag: ({ state }) => state.tag, }); - const testThing = provideStoredObj('testthing', baggage, () => + const testThing = provide(baggage, 'testthing', () => makeThing('test thing'), ); diff --git a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js index 1e96f7c0fa6d..aa4f68a28e5f 100644 --- a/packages/governance/test/swingsetTests/contractGovernor/test-governor.js +++ b/packages/governance/test/swingsetTests/contractGovernor/test-governor.js @@ -113,17 +113,17 @@ test.serial('change electorate', async t => { 'current value of MalleableNumber is 602214090000000000000000', '@@ schedule task for:2, currently: 0 @@', 'Voter Alice voted for {"noChange":["Electorate"]}', - 'Voter Bob voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]}}}', - 'Voter Carol voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]}}}', - 'Voter Dave voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]}}}', + 'Voter Bob voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}}', + 'Voter Carol voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}}', + 'Voter Dave voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}}', 'Voter Emma voted for {"noChange":["Electorate"]}', '@@ tick:1 @@', '@@ tick:2 @@', '&& running a task scheduled for 2. &&', - 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]}}}', + 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}}', 'params update: Electorate', 'current value of MalleableNumber is 602214090000000000000000', - 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]}}})', + 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]}}})', 'Validation complete: true', '@@ schedule task for:4, currently: 2 @@', 'Voter Alice voted for {"changes":{"MalleableNumber":"[299792458n]"}}', @@ -149,14 +149,14 @@ test.serial('brokenUpdateStart', async t => { 'current value of MalleableNumber is 602214090000000000000000', '@@ schedule task for:2, currently: 0 @@', 'Voter Alice voted for {"noChange":["Electorate","MalleableNumber"]}', - 'Voter Bob voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', - 'Voter Carol voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', - 'Voter Dave voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'Voter Bob voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'Voter Carol voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'Voter Dave voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', 'Voter Emma voted for {"noChange":["Electorate","MalleableNumber"]}', '@@ tick:1 @@', '@@ tick:2 @@', '&& running a task scheduled for 2. &&', - 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', 'Validation complete: true', // [`prepareToSetInvitation`](https://github.com/Agoric/agoric-sdk/blob/c6570b015fd23c411e48981bec309b32eedd3a28/packages/governance/src/contractGovernance/paramManager.js#L199-L212) // does a `Promise.all` on 2 calls using the `invite` promise. If that promise @@ -179,19 +179,19 @@ test.serial('changeTwoParams', async t => { 'current value of MalleableNumber is 602214090000000000000000', '@@ schedule task for:2, currently: 0 @@', 'Voter Alice voted for {"noChange":["Electorate","MalleableNumber"]}', - 'Voter Bob voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', - 'Voter Carol voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', - 'Voter Dave voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'Voter Bob voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'Voter Carol voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'Voter Dave voted for {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', 'Voter Emma voted for {"noChange":["Electorate","MalleableNumber"]}', '@@ tick:1 @@', '@@ tick:2 @@', '&& running a task scheduled for 2. &&', - 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', + 'vote outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}}', 'Validation complete: true', 'params update: Electorate,MalleableNumber', 'current value of MalleableNumber is 42', - 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}})', - 'successful outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: Installation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}} ', + 'updated to ({"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}})', + 'successful outcome: {"changes":{"Electorate":{"brand":"[Alleged: Zoe Invitation brand]","value":[{"description":"questionPoser","handle":"[Alleged: InvitationHandle]","installation":"[Alleged: BundleInstallation]","instance":"[Alleged: InstanceHandle]"}]},"MalleableNumber":"[42n]"}} ', ]); }); diff --git a/packages/run-protocol/src/runStake/attestation.js b/packages/run-protocol/src/runStake/attestation.js index 9bf6e2ed9e61..072a9ffcd1b1 100644 --- a/packages/run-protocol/src/runStake/attestation.js +++ b/packages/run-protocol/src/runStake/attestation.js @@ -3,7 +3,7 @@ import { AmountMath, AssetKind } from '@agoric/ertp'; import { E, Far } from '@endo/far'; -import { fit, M, makeCopyBag, makeStore } from '@agoric/store'; +import { fit, M, makeCopyBag, makeStore, provide } from '@agoric/store'; import { assertProposalShape } from '@agoric/zoe/src/contractSupport/index.js'; import { AttKW as KW } from './constants.js'; import { makeAttestationTool } from './attestationTool.js'; @@ -21,20 +21,6 @@ const { details: X } = assert; * }} LienMint */ -/** - * Find-or-create value in store. - * - * @type {(store: Store, key: K, thunk: () => V) => V} - */ -const getOrElse = (store, key, make) => { - if (store.has(key)) { - return store.get(key); - } - const value = make(); - store.init(key, value); - return value; -}; - /** * To support `makeAttestation`, we directly mint attestation payments. * We still use a ZCFMint because returning attestations is done @@ -263,7 +249,7 @@ export const makeAttestationFacets = async (zcf, stakeBrand, lienBridge) => { */ provideAttestationTool: address => { assert.typeof(address, 'string'); - return getOrElse(attMakerByAddress, address, () => + return provide(attMakerByAddress, address, () => makeAttestationTool(address, lienMint, stakeBrand, zcf), ); }, diff --git a/packages/vat-data/src/index.js b/packages/vat-data/src/index.js index 2b968feca935..29bdaec5ffa3 100644 --- a/packages/vat-data/src/index.js +++ b/packages/vat-data/src/index.js @@ -1,84 +1,2 @@ -/* global globalThis */ -import { assert } from '@agoric/assert'; -import { - M, - makeScalarMapStore, - makeScalarWeakMapStore, - makeScalarSetStore, - makeScalarWeakSetStore, -} from '@agoric/store'; - -export { - M, - makeScalarMapStore, - makeScalarWeakMapStore, - makeScalarSetStore, - makeScalarWeakSetStore, -}; - -/** @type {import('./types').VatData} */ -let VatDataGlobal; -if ('VatData' in globalThis) { - assert(globalThis.VatData, 'VatData defined in global as null or undefined'); - VatDataGlobal = globalThis.VatData; -} else { - // XXX this module has been known to get imported (transitively) in cases that - // never use it so we make a version that will satisfy module resolution but - // fail at runtime. - const unvailable = () => assert.fail('VatData unavailable'); - VatDataGlobal = { - defineKind: unvailable, - defineKindMulti: unvailable, - defineDurableKind: unvailable, - defineDurableKindMulti: unvailable, - makeKindHandle: unvailable, - providePromiseWatcher: unvailable, - watchPromise: unvailable, - makeScalarBigMapStore: unvailable, - makeScalarBigWeakMapStore: unvailable, - makeScalarBigSetStore: unvailable, - makeScalarBigWeakSetStore: unvailable, - }; -} - -export const { - defineKind, - defineKindMulti, - defineDurableKind, - defineDurableKindMulti, - makeKindHandle, - providePromiseWatcher, - watchPromise, - makeScalarBigMapStore, - makeScalarBigWeakMapStore, - makeScalarBigSetStore, - makeScalarBigWeakSetStore, -} = VatDataGlobal; - -/** - * When making a multi-facet kind, it's common to pick one facet to expose. E.g., - * - * const makeFoo = (a, b, c, d) => makeFooBase(a, b, c, d).self; - * - * This helper reduces the duplication: - * - * const makeFoo = pickFacet(makeFooBase, 'self'); - * - * @type {import('./types').PickFacet} - */ -export const pickFacet = - (maker, facetName) => - (...args) => - maker(...args)[facetName]; - -/** - * Assign the values of all of the enumerable own properties from the source - * object to their keys in the target object. - * - * @template {{}} T - * @param {T} target - * @param {Partial} source - */ -export const partialAssign = (target, source) => { - Object.assign(target, source); -}; +export * from './vat-data-bindings.js'; +export * from './kind-utils.js'; diff --git a/packages/vat-data/src/index.test-d.ts b/packages/vat-data/src/index.test-d.ts index 1e224a445d5c..5a50769ceb2f 100644 --- a/packages/vat-data/src/index.test-d.ts +++ b/packages/vat-data/src/index.test-d.ts @@ -43,6 +43,7 @@ makeFlorg('notnumber'); type SingleCounterState = { counter: number; name: string }; type SingleCounterContext = { state: SingleCounterState; + self: KindFacet; }; const initCounter = (name: string, str: string): SingleCounterState => ({ counter: 0, @@ -66,12 +67,9 @@ const counterBehavior = { getName: ({ state }: SingleCounterContext) => state.name, }; -const finishCounter = ( - { state }: SingleCounterContext, - counter: KindFacet, -) => { +const finishCounter = ({ state, self }: SingleCounterContext) => { expectType(state.name); - expectType(counter.getCount()); + expectType(self.getCount()); }; const makeCounter = defineKind('counter', initCounter, counterBehavior, { diff --git a/packages/vat-data/src/kind-utils.js b/packages/vat-data/src/kind-utils.js new file mode 100644 index 000000000000..fa8ad5404d9c --- /dev/null +++ b/packages/vat-data/src/kind-utils.js @@ -0,0 +1,59 @@ +import { provide } from '@agoric/store'; +import { defineDurableKind, makeKindHandle } from './vat-data-bindings.js'; + +export const dropContext = + fn => + (_, ...args) => + fn(...args); +// @ts-expect-error TODO statically recognize harden +harden(dropContext); + +export const provideKindHandle = (baggage, kindName) => + provide(baggage, `${kindName}_kindHandle`, () => makeKindHandle(kindName)); +// @ts-expect-error TODO statically recognize harden +harden(provideKindHandle); + +/** + * By analogy with how `Array.prototype.map` will map the elements of + * an array to transformed elements of an array of the same shape, + * `objectMap` will do likewise for the string-named own enumerable + * properties of an object. + * + * See the comment at + * https://github.com/Agoric/agoric-sdk/pull/5674#discussion_r908883510 + * TODO need to move this to a common place. + * + * @template T, U + * @template {keyof T} K + * @param {{ [K2 in keyof T]: T[K2] }} original + * @param {(pair: [K, T[K]]) => [K, U]} mapPairFn + * @returns {Record} + */ +export const objectMap = (original, mapPairFn) => { + const ents = /** @type {[K, T[K]][]} */ (Object.entries(original)); + const mapEnts = ents.map(ent => mapPairFn(ent)); + // @ts-expect-error TODO statically recognize harden + return /** @type {Record} */ (harden(Object.fromEntries(mapEnts))); +}; + +/** + * @template T + * @param {import('@agoric/store').MapStore} baggage + * @param {string} kindName + * @param {T} methods + * @param {import('./types.js').DefineKindOptions} [options] + * @returns {T} + */ +export const ProvideFar = (baggage, kindName, methods, options = undefined) => { + const kindHandle = provideKindHandle(baggage, kindName); + const behavior = objectMap(methods, ([k, m]) => [k, dropContext(m)]); + const makeSingleton = defineDurableKind( + kindHandle, + () => ({}), + behavior, + options, + ); + return provide(baggage, `the_${kindName}`, () => makeSingleton()); +}; +// @ts-expect-error TODO statically recognize harden +harden(ProvideFar); diff --git a/packages/vat-data/src/types.d.ts b/packages/vat-data/src/types.d.ts index 43c0b11ed24a..8d54f6dd7226 100644 --- a/packages/vat-data/src/types.d.ts +++ b/packages/vat-data/src/types.d.ts @@ -29,6 +29,7 @@ type KindFacets = { [FacetKey in keyof B]: KindFacet; }; +type KindContext = { state: S; self: KindFacet }; type MultiKindContext = { state: S; facets: KindFacets }; type PlusContext = (c: C, ...args: Parameters) => ReturnType; @@ -39,21 +40,25 @@ declare class DurableKindHandleClass { } export type DurableKindHandle = DurableKindHandleClass; +type DefineKindOptions = { + finish?: (context: C) => void; + durable?: boolean; + fakeDurable?: boolean; +}; + export type VatData = { // virtual kinds defineKind: ( tag: string, init: (...args: P) => S, facet: F, - options?: { - finish?: (context: { state: S }, kind: KindFacet) => void; - }, + options?: DefineKindOptions>, ) => (...args: P) => KindFacet; defineKindMulti: ( tag: string, init: (...args: P) => S, behavior: B, - options?: { finish?: (context: MultiKindContext) => void }, + options?: DefineKindOptions>, ) => (...args: P) => KindFacets; // durable kinds @@ -62,15 +67,13 @@ export type VatData = { kindHandle: DurableKindHandle, init: (...args: P) => S, facet: F, - options?: { - finish?: (context: { state: S }, kind: KindFacet) => void; - }, + options?: DefineKindOptions>, ) => (...args: P) => KindFacet; defineDurableKindMulti: ( kindHandle: DurableKindHandle, init: (...args: P) => S, behavior: B, - options?: { finish?: (context: MultiKindContext) => void }, + options?: DefineKindOptions>, ) => (...args: P) => KindFacets; providePromiseWatcher: unknown; diff --git a/packages/vat-data/src/vat-data-bindings.js b/packages/vat-data/src/vat-data-bindings.js new file mode 100644 index 000000000000..6ef3b570f178 --- /dev/null +++ b/packages/vat-data/src/vat-data-bindings.js @@ -0,0 +1,107 @@ +/* global globalThis */ + +import { assert } from '@agoric/assert'; +import { + M, + makeScalarMapStore, + makeScalarWeakMapStore, + makeScalarSetStore, + makeScalarWeakSetStore, + provide, +} from '@agoric/store'; + +export { + M, + makeScalarMapStore, + makeScalarWeakMapStore, + makeScalarSetStore, + makeScalarWeakSetStore, +}; + +/** @type {import('./types').VatData} */ +let VatDataGlobal; +if ('VatData' in globalThis) { + assert(globalThis.VatData, 'VatData defined in global as null or undefined'); + VatDataGlobal = globalThis.VatData; +} else { + // XXX this module has been known to get imported (transitively) in cases that + // never use it so we make a version that will satisfy module resolution but + // fail at runtime. + const unvailable = () => assert.fail('VatData unavailable'); + VatDataGlobal = { + defineKind: unvailable, + defineKindMulti: unvailable, + defineDurableKind: unvailable, + defineDurableKindMulti: unvailable, + makeKindHandle: unvailable, + providePromiseWatcher: unvailable, + watchPromise: unvailable, + makeScalarBigMapStore: unvailable, + makeScalarBigWeakMapStore: unvailable, + makeScalarBigSetStore: unvailable, + makeScalarBigWeakSetStore: unvailable, + }; +} + +export const { + defineKind, + defineKindMulti, + defineDurableKind, + defineDurableKindMulti, + makeKindHandle, + providePromiseWatcher, + watchPromise, + makeScalarBigMapStore, + makeScalarBigWeakMapStore, + makeScalarBigSetStore, + makeScalarBigWeakSetStore, +} = VatDataGlobal; + +/** + * When making a multi-facet kind, it's common to pick one facet to expose. E.g., + * + * const makeFoo = (a, b, c, d) => makeFooBase(a, b, c, d).self; + * + * This helper reduces the duplication: + * + * const makeFoo = pickFacet(makeFooBase, 'self'); + * + * @type {import('./types').PickFacet} + */ +export const pickFacet = + (maker, facetName) => + (...args) => + maker(...args)[facetName]; +// @ts-expect-error TODO statically recognize harden +harden(pickFacet); + +/** + * Assign the values of all of the enumerable own properties from the source + * object to their keys in the target object. + * + * @template {{}} T + * @param {T} target + * @param {Partial} source + */ +export const partialAssign = (target, source) => { + Object.assign(target, source); +}; +// @ts-expect-error TODO statically recognize harden +harden(partialAssign); + +export const provideDurableMapStore = (baggage, name) => + provide(baggage, name, () => makeScalarBigMapStore(name, { durable: true })); +// @ts-expect-error TODO statically recognize harden +harden(provideDurableMapStore); + +export const provideDurableWeakMapStore = (baggage, name) => + provide(baggage, name, () => + makeScalarBigWeakMapStore(name, { durable: true }), + ); +// @ts-expect-error TODO statically recognize harden +harden(provideDurableWeakMapStore); + +export const provideDurableSetStore = (baggage, name) => + provide(baggage, name, () => makeScalarBigSetStore(name, { durable: true })); +// @ts-expect-error TODO statically recognize harden +harden(provideDurableSetStore); diff --git a/packages/vats/src/vat-zoe.js b/packages/vats/src/vat-zoe.js index 3347cad9c13b..5a9cf0c1da75 100644 --- a/packages/vats/src/vat-zoe.js +++ b/packages/vats/src/vat-zoe.js @@ -1,7 +1,7 @@ import { Far } from '@endo/far'; import { makeZoeKit } from '@agoric/zoe'; -export function buildRootObject(vatPowers) { +export function buildRootObject(vatPowers, _vatParams, zoeBaggage) { return Far('root', { buildZoe: async (adminVat, feeIssuerConfig, zcfBundleName) => { const shutdownZoeVat = vatPowers.exitVatWithFailure; @@ -11,6 +11,7 @@ export function buildRootObject(vatPowers) { shutdownZoeVat, feeIssuerConfig, { name: zcfBundleName }, + zoeBaggage, ); return harden({ zoeService, diff --git a/packages/wallet/api/test/test-lib-wallet.js b/packages/wallet/api/test/test-lib-wallet.js index 8c9615e83d39..d6f168ae8eb4 100644 --- a/packages/wallet/api/test/test-lib-wallet.js +++ b/packages/wallet/api/test/test-lib-wallet.js @@ -469,7 +469,7 @@ test('lib-wallet dapp suggests issuer, instance, installation petnames', async ( }, ], currentAmountSlots: { - body: '{"brand":{"@qclass":"slot","iface":"Alleged: Zoe Invitation brand","index":0},"value":[{"description":"getRefund","handle":{"@qclass":"slot","iface":"Alleged: InvitationHandle","index":1},"installation":{"@qclass":"slot","iface":"Alleged: Installation","index":2},"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":3}}]}', + body: '{"brand":{"@qclass":"slot","iface":"Alleged: Zoe Invitation brand","index":0},"value":[{"description":"getRefund","handle":{"@qclass":"slot","iface":"Alleged: InvitationHandle","index":1},"installation":{"@qclass":"slot","iface":"Alleged: BundleInstallation","index":2},"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":3}}]}', slots: [ { kind: 'brand', petname: 'zoe invite' }, { kind: 'unnamed', petname: 'unnamed-4' }, @@ -556,7 +556,7 @@ test('lib-wallet dapp suggests issuer, instance, installation petnames', async ( }, ], currentAmountSlots: { - body: '{"brand":{"@qclass":"slot","iface":"Alleged: Zoe Invitation brand","index":0},"value":[{"description":"getRefund","handle":{"@qclass":"slot","iface":"Alleged: InvitationHandle","index":1},"installation":{"@qclass":"slot","iface":"Alleged: Installation","index":2},"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":3}}]}', + body: '{"brand":{"@qclass":"slot","iface":"Alleged: Zoe Invitation brand","index":0},"value":[{"description":"getRefund","handle":{"@qclass":"slot","iface":"Alleged: InvitationHandle","index":1},"installation":{"@qclass":"slot","iface":"Alleged: BundleInstallation","index":2},"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":3}}]}', slots: [ { kind: 'brand', petname: 'zoe invite' }, { kind: 'unnamed', petname: 'unnamed-4' }, @@ -695,7 +695,7 @@ test('lib-wallet dapp suggests issuer, instance, installation petnames', async ( }, ], currentAmountSlots: { - body: '{"brand":{"@qclass":"slot","iface":"Alleged: Zoe Invitation brand","index":0},"value":[{"description":"getRefund","handle":{"@qclass":"slot","iface":"Alleged: InvitationHandle","index":1},"installation":{"@qclass":"slot","iface":"Alleged: Installation","index":2},"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":3}}]}', + body: '{"brand":{"@qclass":"slot","iface":"Alleged: Zoe Invitation brand","index":0},"value":[{"description":"getRefund","handle":{"@qclass":"slot","iface":"Alleged: InvitationHandle","index":1},"installation":{"@qclass":"slot","iface":"Alleged: BundleInstallation","index":2},"instance":{"@qclass":"slot","iface":"Alleged: InstanceHandle","index":3}}]}', slots: [ { kind: 'brand', petname: 'zoe invite' }, { kind: 'unnamed', petname: 'unnamed-4' }, diff --git a/packages/zoe/src/makeHandle.js b/packages/zoe/src/makeHandle.js index 4bb11739a0cc..9ab3e9895eb0 100644 --- a/packages/zoe/src/makeHandle.js +++ b/packages/zoe/src/makeHandle.js @@ -1,8 +1,29 @@ // @ts-check import { assert } from '@agoric/assert'; +import { provide } from '@agoric/store'; +import { defineDurableKind, makeKindHandle } from '@agoric/vat-data'; import { Far } from '@endo/marshal'; +/** + * @template {string} H + * // ??? Purposeful use of `any`. Should we use `unknown` instead? + * @param {MapStore} baggage + * @param {H} handleType + * @returns {() => Handle} + */ +export const defineDurableHandle = (baggage, handleType) => { + assert.typeof(handleType, 'string', 'handleType must be a string'); + const durableHandleKindHandle = provide( + baggage, + `${handleType}KindHandle`, + () => makeKindHandle(`${handleType}Handle`), + ); + const makeHandle = defineDurableKind(durableHandleKindHandle, () => ({}), {}); + return /** @type {() => Handle} */ (makeHandle); +}; +harden(defineDurableHandle); + /** * Create an opaque handle object. * @@ -11,8 +32,8 @@ import { Far } from '@endo/marshal'; * @returns {Handle} */ export const makeHandle = handleType => { - // This assert ensures that handleType is referenced. assert.typeof(handleType, 'string', 'handleType must be a string'); // Return the intersection type (really just an empty object). return /** @type {Handle} */ (Far(`${handleType}Handle`)); }; +harden(makeHandle); diff --git a/packages/zoe/src/zoeService/installationStorage.js b/packages/zoe/src/zoeService/installationStorage.js index 280176cce6bd..3dd153a1c699 100644 --- a/packages/zoe/src/zoeService/installationStorage.js +++ b/packages/zoe/src/zoeService/installationStorage.js @@ -1,21 +1,51 @@ // @ts-check import { assert, details as X } from '@agoric/assert'; -import { Far } from '@endo/marshal'; import { E } from '@endo/eventual-send'; import { makeWeakStore } from '@agoric/store'; +import { + defineDurableKind, + makeScalarBigMapStore, + provideKindHandle, +} from '@agoric/vat-data'; /** @typedef { import('@agoric/swingset-vat').BundleID} BundleID */ /** * @param {GetBundleCapForID} getBundleCapForID + * @param {MapStore} [zoeBaggage] */ -export const makeInstallationStorage = getBundleCapForID => { +export const makeInstallationStorage = ( + getBundleCapForID, + zoeBaggage = makeScalarBigMapStore('zoe baggage', { durable: true }), +) => { /** @type {WeakStore} */ const installationsBundleCap = makeWeakStore('installationsBundleCap'); /** @type {WeakStore} */ const installationsBundle = makeWeakStore('installationsBundle'); + const bundleIDInstallationKindHandle = provideKindHandle( + zoeBaggage, + 'BundleIDInstallation', + ); + + const bundleInstallationKindHandle = provideKindHandle( + zoeBaggage, + 'BundleInstallation', + ); + + const makeBundleIDInstallation = defineDurableKind( + bundleIDInstallationKindHandle, + () => ({}), + { getBundle: _context => assert.fail('bundleID-based Installation') }, + ); + + const makeBundleInstallation = defineDurableKind( + bundleInstallationKindHandle, + bundle => ({ bundle }), + { getBundle: ({ state: { bundle } }) => bundle }, + ); + /** * Create an installation from a bundle ID or a full bundle. If we are * given a bundle ID, wait for the corresponding code bundle to be received @@ -36,11 +66,7 @@ export const makeInstallationStorage = getBundleCapForID => { /** @type {Installation} */ // @ts-expect-error cast - const installation = Far('Installation', { - getBundle: () => { - throw Error('bundleID-based Installation'); - }, - }); + const installation = makeBundleIDInstallation(); installationsBundleCap.init(installation, { bundleCap, bundleID }); return installation; }; @@ -50,9 +76,7 @@ export const makeInstallationStorage = getBundleCapForID => { assert.typeof(bundle, 'object', 'a bundle must be provided'); /** @type {Installation} */ // @ts-expect-error cast - const installation = Far('Installation', { - getBundle: () => bundle, - }); + const installation = makeBundleInstallation(bundle); installationsBundle.init(installation, bundle); return installation; }; @@ -62,7 +86,7 @@ export const makeInstallationStorage = getBundleCapForID => { // Bundle is a very open-ended type and we must decide here // whether to treat it as either a HashBundle or SourceBundle. // So, we cast it down and inspect it. - const bundle = /** @type {unknown} */ (allegedBundle); + const bundle = /** @type {unknown} */ (harden(allegedBundle)); assert.typeof(bundle, 'object', 'a bundle must be provided'); assert(bundle !== null, 'a bundle must be provided'); const { moduleFormat } = bundle; diff --git a/packages/zoe/src/zoeService/startInstance.js b/packages/zoe/src/zoeService/startInstance.js index 80e6e1fa1aac..0dfc08239c18 100644 --- a/packages/zoe/src/zoeService/startInstance.js +++ b/packages/zoe/src/zoeService/startInstance.js @@ -4,9 +4,10 @@ import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; import { Far, passStyleOf } from '@endo/marshal'; import { makeWeakStore } from '@agoric/store'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; import { makeZoeSeatAdminKit } from './zoeSeat.js'; -import { makeHandle } from '../makeHandle.js'; +import { defineDurableHandle } from '../makeHandle.js'; import { handlePKitWarning } from '../handleWarning.js'; const { details: X, quote: q } = assert; @@ -15,13 +16,19 @@ const { details: X, quote: q } = assert; * @param {Promise} zoeServicePromise * @param {MakeZoeInstanceStorageManager} makeZoeInstanceStorageManager * @param {UnwrapInstallation} unwrapInstallation + * @param {MapStore} [zoeBaggage] * @returns {import('./utils.js').StartInstance} */ export const makeStartInstance = ( zoeServicePromise, makeZoeInstanceStorageManager, unwrapInstallation, + zoeBaggage = makeScalarBigMapStore('zoe baggage', { durable: true }), ) => { + const makeInstanceHandle = defineDurableHandle(zoeBaggage, 'Instance'); + // TODO(MSM): Should be 'Seat' rather than 'SeatHandle' + const makeSeatHandle = defineDurableHandle(zoeBaggage, 'SeatHandle'); + const startInstance = async ( installationP, uncleanIssuerKeywordRecord = harden({}), @@ -49,7 +56,7 @@ export const makeStartInstance = ( ); } - const instance = makeHandle('Instance'); + const instance = makeInstanceHandle(); const zoeInstanceStorageManager = await makeZoeInstanceStorageManager( installation, @@ -111,7 +118,7 @@ export const makeStartInstance = ( handlePKitWarning(offerResultPromiseKit); const exitObjPromiseKit = makePromiseKit(); handlePKitWarning(exitObjPromiseKit); - const seatHandle = makeHandle('SeatHandle'); + const seatHandle = makeSeatHandle(); const { userSeat, notifier, zoeSeatAdmin } = makeZoeSeatAdminKit( initialAllocation, diff --git a/packages/zoe/src/zoeService/zoe.js b/packages/zoe/src/zoeService/zoe.js index 13b4ebe4eaf9..078fbb18a1a2 100644 --- a/packages/zoe/src/zoeService/zoe.js +++ b/packages/zoe/src/zoeService/zoe.js @@ -19,6 +19,7 @@ import { AssetKind } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makePromiseKit } from '@endo/promise-kit'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; import { makeZoeStorageManager } from './zoeStorageManager.js'; import { makeStartInstance } from './startInstance.js'; @@ -37,6 +38,7 @@ import { createFeeMint } from './feeMint.js'; * available to a vat. * @param {FeeIssuerConfig} feeIssuerConfig * @param {ZCFSpec} [zcfSpec] - Pointer to the contract facet bundle. + * @param {MapStore} [zoeBaggage] * @returns {{ * zoeService: ZoeService, * feeMintAccess: FeeMintAccess, @@ -51,6 +53,7 @@ const makeZoeKit = ( displayInfo: harden({ decimalPlaces: 6, assetKind: AssetKind.NAT }), }, zcfSpec = { name: 'zcf' }, + zoeBaggage = makeScalarBigMapStore('zoe baggage', { durable: true }), ) => { // We must pass the ZoeService to `makeStartInstance` before it is // defined. See below where the promise is resolved. @@ -95,6 +98,7 @@ const makeZoeKit = ( shutdownZoeVat, feeIssuer, feeBrand, + zoeBaggage, ); // Pass the capabilities necessary to create E(zoe).startInstance diff --git a/packages/zoe/src/zoeService/zoeStorageManager.js b/packages/zoe/src/zoeService/zoeStorageManager.js index 854e31d5d5ff..f67b126653f4 100644 --- a/packages/zoe/src/zoeService/zoeStorageManager.js +++ b/packages/zoe/src/zoeService/zoeStorageManager.js @@ -2,7 +2,10 @@ import { AssetKind, makeIssuerKit } from '@agoric/ertp'; import { Far } from '@endo/marshal'; -import { makeScalarBigWeakMapStore } from '@agoric/vat-data'; +import { + makeScalarBigMapStore, + makeScalarBigWeakMapStore, +} from '@agoric/vat-data'; import { makeIssuerStorage } from '../issuerStorage.js'; import { makeAndStoreInstanceRecord } from '../instanceRecordStorage.js'; @@ -32,6 +35,7 @@ import './internal-types.js'; * @param {ShutdownWithFailure} shutdownZoeVat * @param {Issuer} feeIssuer * @param {Brand} feeBrand + * @param {MapStore} [zoeBaggage] * @returns {ZoeStorageManager} */ export const makeZoeStorageManager = ( @@ -41,6 +45,7 @@ export const makeZoeStorageManager = ( shutdownZoeVat, feeIssuer, feeBrand, + zoeBaggage = makeScalarBigMapStore('zoe baggage', { durable: true }), ) => { // issuerStorage contains the issuers that the ZoeService knows // about, as well as information about them such as their brand, @@ -90,7 +95,7 @@ export const makeZoeStorageManager = ( installBundleID, unwrapInstallation, getBundleIDFromInstallation, - } = makeInstallationStorage(getBundleCapForID); + } = makeInstallationStorage(getBundleCapForID, zoeBaggage); const proposalSchemas = makeScalarBigWeakMapStore('proposal schemas'); diff --git a/packages/zoe/test/swingsetTests/brokenContracts/test-crashingContract.js b/packages/zoe/test/swingsetTests/brokenContracts/test-crashingContract.js index ce049a824870..c1826a643347 100644 --- a/packages/zoe/test/swingsetTests/brokenContracts/test-crashingContract.js +++ b/packages/zoe/test/swingsetTests/brokenContracts/test-crashingContract.js @@ -27,6 +27,7 @@ async function main(argv) { config.defaultManagerType = 'xs-worker'; await generateBundlesP; config.bundles = { zcf: { bundle: zcfBundle }, ...contractBundles }; + config.relaxDurabilityRules = true; const controller = await buildVatController(config, argv); await controller.run(); return controller.dump(); diff --git a/packages/zoe/test/swingsetTests/offerArgs/test-offerArgs.js b/packages/zoe/test/swingsetTests/offerArgs/test-offerArgs.js index 0b9be522379c..4a885b903f97 100644 --- a/packages/zoe/test/swingsetTests/offerArgs/test-offerArgs.js +++ b/packages/zoe/test/swingsetTests/offerArgs/test-offerArgs.js @@ -51,6 +51,7 @@ test.before(async t => { const config = { bootstrap: 'bootstrap', vats }; config.bundles = { zcf: { bundle: zcfBundle }, ...contractBundles }; config.defaultManagerType = 'xs-worker'; + config.relaxDurabilityRules = true; const step4 = Date.now(); const ktime = `${(step2 - start) / 1000}s kernel`; diff --git a/packages/zoe/test/swingsetTests/zoe/test-zoe.js b/packages/zoe/test/swingsetTests/zoe/test-zoe.js index b6952a03ba49..d923f1685ca3 100644 --- a/packages/zoe/test/swingsetTests/zoe/test-zoe.js +++ b/packages/zoe/test/swingsetTests/zoe/test-zoe.js @@ -66,6 +66,7 @@ test.before(async t => { }; const config = { bootstrap: 'bootstrap', vats, bundles }; config.defaultManagerType = 'xs-worker'; + config.relaxDurabilityRules = true; const step4 = Date.now(); const ktime = `${(step2 - start) / 1000}s kernel`;