diff --git a/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts b/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts index 910234eedd9..30bd4092dd2 100644 --- a/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts +++ b/packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts @@ -288,7 +288,7 @@ const checkFlow1 = async ( // restart Zoe // /////// Upgrading //////////////////////////////// await buildAndExecuteProposal( - '@agoric/builders/scripts/vats/null-upgrade-zoe-proposal.js', + '@agoric/builders/scripts/vats/upgrade-zoe-proposal.js', ); await buyer.tryExitOffer(`${collateralBrandKey}-bid3`); diff --git a/packages/builders/package.json b/packages/builders/package.json index 5b603d062f2..d771ca784ab 100644 --- a/packages/builders/package.json +++ b/packages/builders/package.json @@ -8,9 +8,8 @@ "scripts": { "build": "exit 0", "build:add-STARS-proposal": "echo This command has been deprecated. Please run this instead: agoric run scripts/inter-protocol/add-STARS.js", - "build:restart-vats-proposal": "echo echo This command has been deprecated. Please run this instead: agoric run scripts/vats/restart-vats.js", + "build:restart-vats-proposal": "echo This command has been deprecated. Please run this instead: agoric run scripts/vats/restart-vats.js", "build:zcf-proposal": "echo This command has been deprecated. Please run this instead: agoric run scripts/vats/replace-zoe.js", - "build:null-upgrade-zoe-proposal": "echo This command has been deprecated. Please run this instead: agoric run scripts/vats/replace-zoe.js", "prepack": "tsc --build tsconfig.build.json", "postpack": "git clean -f '*.d.ts*'", "test": "ava", diff --git a/packages/builders/scripts/vats/upgrade-zoe.js b/packages/builders/scripts/vats/upgrade-zoe.js index a9ea1fdec9e..e7503efbb13 100644 --- a/packages/builders/scripts/vats/upgrade-zoe.js +++ b/packages/builders/scripts/vats/upgrade-zoe.js @@ -3,7 +3,7 @@ import { makeHelpers } from '@agoric/deploy-script-support'; /** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ export const defaultProposalBuilder = async ({ publishRef, install }) => harden({ - sourceSpec: '@agoric/vats/src/proposals/null-upgrade-zoe-proposal.js', + sourceSpec: '@agoric/vats/src/proposals/upgrade-zoe-proposal.js', getManifestCall: [ 'getManifestForUpgradingZoe', { @@ -14,5 +14,5 @@ export const defaultProposalBuilder = async ({ publishRef, install }) => export default async (homeP, endowments) => { const { writeCoreProposal } = await makeHelpers(homeP, endowments); - await writeCoreProposal('null-upgrade-zoe', defaultProposalBuilder); + await writeCoreProposal('upgrade-zoe', defaultProposalBuilder); }; diff --git a/packages/vats/src/proposals/null-upgrade-zoe-proposal.js b/packages/vats/src/proposals/upgrade-zoe-proposal.js similarity index 93% rename from packages/vats/src/proposals/null-upgrade-zoe-proposal.js rename to packages/vats/src/proposals/upgrade-zoe-proposal.js index 0de11165420..6ae75e32088 100644 --- a/packages/vats/src/proposals/null-upgrade-zoe-proposal.js +++ b/packages/vats/src/proposals/upgrade-zoe-proposal.js @@ -13,7 +13,7 @@ import { E } from '@endo/far'; * @param {object} options * @param {{ zoeRef: VatSourceRef; zcfRef: VatSourceRef }} options.options */ -export const nullUpgradeZoe = async ( +export const upgradeZoe = async ( { consume: { vatAdminSvc, vatStore } }, options, ) => { @@ -30,7 +30,7 @@ export const nullUpgradeZoe = async ( export const getManifestForUpgradingZoe = (_powers, { zoeRef }) => ({ manifest: { - [nullUpgradeZoe.name]: { + [upgradeZoe.name]: { consume: { vatAdminSvc: 'vatAdminSvc', vatStore: 'vatStore', diff --git a/packages/zoe/src/contractFacet/zcfZygote.js b/packages/zoe/src/contractFacet/zcfZygote.js index f44fb130f06..ed15761a015 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -414,6 +414,7 @@ export const makeZCFZygote = async ( instanceRecHolder = makeInstanceRecord(instanceRecordFromZoe); instantiateIssuerStorage(issuerStorageFromZoe); zcfBaggage.init('instanceRecHolder', instanceRecHolder); + zcfBaggage.init('repairedContractCompletionWatcher', true); const { privateArgsShape } = meta; if (privateArgsShape) { @@ -466,6 +467,13 @@ export const makeZCFZygote = async ( instanceRecHolder = zcfBaggage.get('instanceRecHolder'); initSeatMgrAndMintKind(); + await null; + if (!zcfBaggage.has('repairedContractCompletionWatcher')) { + await E(zoeInstanceAdmin).repairContractCompletionWatcher(); + console.log(`Repaired contract completion watcher`); + zcfBaggage.init('repairedContractCompletionWatcher', true); + } + const { privateArgsShape } = meta; if (privateArgsShape) { mustMatch(privateArgs, privateArgsShape, 'privateArgs'); diff --git a/packages/zoe/src/internal-types.js b/packages/zoe/src/internal-types.js index ee794b555b7..15643717dce 100644 --- a/packages/zoe/src/internal-types.js +++ b/packages/zoe/src/internal-types.js @@ -137,6 +137,7 @@ * @property {(strings: Array) => void} setOfferFilter * @property {() => Array} getOfferFilter * @property {(seatHandle: SeatHandle) => Subscriber} getExitSubscriber + * @property {() => void} repairContractCompletionWatcher */ /** diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index cbc330fa374..410902f78d0 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -196,6 +196,7 @@ export const InstanceAdminI = M.interface('InstanceAdmin', { getOfferFilter: M.call().returns(M.arrayOf(M.string())), getExitSubscriber: M.call(SeatShape).returns(SubscriberShape), isBlocked: M.call(M.string()).returns(M.boolean()), + repairContractCompletionWatcher: M.call().returns(), }); export const InstanceStorageManagerIKit = harden({ diff --git a/packages/zoe/src/zoeService/startInstance.js b/packages/zoe/src/zoeService/startInstance.js index 03b0a2a7c2c..d3eb173abde 100644 --- a/packages/zoe/src/zoeService/startInstance.js +++ b/packages/zoe/src/zoeService/startInstance.js @@ -6,12 +6,21 @@ import { makeScalarBigMapStore, provideDurableWeakMapStore, prepareExoClass, + prepareExo, + watchPromise, } from '@agoric/vat-data'; import { initEmpty } from '@agoric/store'; +import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; import { defineDurableHandle } from '../makeHandle.js'; import { makeInstanceAdminMaker } from './instanceAdminStorage.js'; -import { AdminFacetI, InstanceAdminI } from '../typeGuards.js'; +import { + AdminFacetI, + InstanceAdminI, + InstanceAdminShape, +} from '../typeGuards.js'; + +// import '../internal-types.js'; /** @typedef {import('@agoric/vat-data').Baggage} Baggage */ /** @typedef { import('@agoric/swingset-vat').BundleCap} BundleCap */ @@ -53,10 +62,45 @@ export const makeStartInstance = ( const InstanceAdminStateShape = harden({ instanceStorage: M.remotable('ZoeInstanceStorageManager'), instanceAdmin: M.remotable('InstanceAdmin'), - seatHandleToSeatAdmin: M.remotable(), + seatHandleToSeatAdmin: M.remotable(), // seatHandleToSeatAdmin, but putting that string here is backwards-incompatible adminNode: M.remotable('adminNode'), }); + /** @type {import('@agoric/swingset-liveslots').PromiseWatcher]>} */ + const watcher = prepareExo( + zoeBaggage, + 'InstanceCompletionWatcher', + M.interface('InstanceCompletionWatcher', { + onFulfilled: M.call( + M.any(), + InstanceAdminShape, + M.remotable('adminNode'), + ).returns(), + onRejected: M.call( + M.any(), + InstanceAdminShape, + M.remotable('adminNode'), + ).returns(), + }), + { + onFulfilled: (completion, instanceAdmin) => + instanceAdmin.exitAllSeats(completion), + onRejected: (/** @type {Error} */ reason, instanceAdmin, adminNode) => { + if (isUpgradeDisconnection(reason)) { + console.log(`resetting promise watcher after upgrade`, reason); + // eslint-disable-next-line no-use-before-define + watchForAdminNodeDone(adminNode, instanceAdmin); + } else { + instanceAdmin.failAllSeats(reason); + } + }, + }, + ); + + const watchForAdminNodeDone = (adminNode, instAdmin) => { + watchPromise(E(adminNode).done(), watcher, instAdmin, adminNode); + }; + const makeZoeInstanceAdmin = prepareExoClass( zoeBaggage, 'zoeInstanceAdmin', @@ -131,10 +175,10 @@ export const makeStartInstance = ( replaceAllocations(seatHandleAllocations) { const { state } = this; try { - seatHandleAllocations.forEach(({ seatHandle, allocation }) => { + for (const { seatHandle, allocation } of seatHandleAllocations) { const zoeSeatAdmin = state.seatHandleToSeatAdmin.get(seatHandle); zoeSeatAdmin.replaceAllocation(allocation); - }); + } } catch (err) { // nothing for Zoe to do if the termination fails void E(state.adminNode).terminateWithFailure(err); @@ -161,6 +205,10 @@ export const makeStartInstance = ( const { state } = this; return state.instanceAdmin.isBlocked(string); }, + repairContractCompletionWatcher() { + const { state, self } = this; + void watchForAdminNodeDone(state.adminNode, self); + }, }, { stateShape: InstanceAdminStateShape, @@ -278,13 +326,7 @@ export const makeStartInstance = ( ); zoeInstanceStorageManager.initInstanceAdmin(instanceHandle, instanceAdmin); - E.when( - E(adminNode).done(), - completion => { - instanceAdmin.exitAllSeats(completion); - }, - reason => instanceAdmin.failAllSeats(reason), - ); + void watchForAdminNodeDone(adminNode, instanceAdmin); /** @type {ZoeInstanceAdmin} */ const zoeInstanceAdminForZcf = makeZoeInstanceAdmin( diff --git a/packages/zoe/test/swingsetTests/zoe/test-zoe-upgrade.js b/packages/zoe/test/swingsetTests/zoe/test-zoe-upgrade.js index 226a2d1450a..68b503ffcf3 100644 --- a/packages/zoe/test/swingsetTests/zoe/test-zoe-upgrade.js +++ b/packages/zoe/test/swingsetTests/zoe/test-zoe-upgrade.js @@ -1,5 +1,7 @@ import '@agoric/swingset-liveslots/tools/prepare-test-env.js'; import test from 'ava'; + +import bundleSource from '@endo/bundle-source'; import { buildVatController } from '@agoric/swingset-vat'; import { kunser } from '@agoric/kmarshal'; @@ -53,6 +55,18 @@ test('zoe vat upgrade trauma', async t => { return awaitRun(kpid); }; + const restartVatAdminVat = async controller => { + const vaBundle = await bundleSource( + new URL( + '../../../../SwingSet/src/vats/vat-admin/vat-vat-admin.js', + import.meta.url, + ).pathname, + ); + const bundleID = await controller.validateAndInstallBundle(vaBundle); + controller.upgradeStaticVat('vatAdmin', true, bundleID, {}); + await controller.run(); + }; + /** * @see {@link ../upgradeCoveredCall/bootstrap-coveredCall-service-upgrade.js} */ @@ -227,13 +241,16 @@ test('zoe vat upgrade trauma', async t => { pausedFlows.push({ result, remainingSteps: flow.slice(i) }); } + // Null-upgrade vatAdmin. + await restartVatAdminVat(c); + // Null-upgrade Zoe. - const { incarnationNumber } = await messageToVat( + const { incarnationNumber: zoeIncarnationNumber } = await messageToVat( 'bootstrap', 'upgradeVat', zoeVatConfig, ); - t.is(incarnationNumber, 1, 'Zoe vat must be upgraded'); + t.is(zoeIncarnationNumber, 1, 'Zoe vat must be upgraded'); // Verify a complete run in the new Zoe. await doSteps('post-upgrade', flow); @@ -241,6 +258,6 @@ test('zoe vat upgrade trauma', async t => { // Verify completion of each paused flow. for (const { result, remainingSteps } of pausedFlows) { const [beforeStepName] = remainingSteps[0]; - await doSteps(`resumed-${beforeStepName}`, flow, result); + await doSteps(`resumed-${beforeStepName}`, remainingSteps, result); } }); diff --git a/packages/zoe/test/unitTests/zcf/test-zcf.js b/packages/zoe/test/unitTests/zcf/test-zcf.js index ed4728fd615..a8e68a05d4f 100644 --- a/packages/zoe/test/unitTests/zcf/test-zcf.js +++ b/packages/zoe/test/unitTests/zcf/test-zcf.js @@ -1273,6 +1273,45 @@ test('numWantsSatisfied: no', async t => { await zcfSeat.exit(); t.is(await E(userSeat).numWantsSatisfied(), 0); + + t.deepEqual(await E(E(userSeat).getExitSubscriber()).getUpdateSince(), { + updateCount: undefined, + value: undefined, + }); +}); + +test('numWantsSatisfied: fail', async t => { + const { zcf } = await setupZCFTest(); + const doubloonMint = await zcf.makeZCFMint('Doubloons'); + const yenMint = await zcf.makeZCFMint('Yen'); + const { brand: doubloonBrand } = doubloonMint.getIssuerRecord(); + const { brand: yenBrand } = yenMint.getIssuerRecord(); + const yenAmount = AmountMath.make(yenBrand, 100n); + const proposal = harden({ + give: { DownPayment: yenAmount }, + want: { Bonus: AmountMath.make(doubloonBrand, 1_000_000n) }, + }); + + const { zcfSeat: mintSeat, userSeat: payoutSeat } = zcf.makeEmptySeatKit(); + yenMint.mintGains(harden({ Cost: yenAmount }), mintSeat); + mintSeat.exit(); + const payout = await E(payoutSeat).getPayout('Cost'); + const payment = { DownPayment: payout }; + + const { zcfSeat, userSeat } = await makeOffer( + zcf.getZoeService(), + zcf, + proposal, + payment, + ); + + void zcfSeat.fail(Error('whatever')); + t.is(await E(userSeat).numWantsSatisfied(), 0); + + await t.throwsAsync( + () => E(E(userSeat).getExitSubscriber()).getUpdateSince(), + { message: 'whatever' }, + ); }); test('numWantsSatisfied: yes', async t => { @@ -1293,6 +1332,11 @@ test('numWantsSatisfied: yes', async t => { await zcfSeat.exit(); t.is(await E(userSeat).numWantsSatisfied(), 1); + + t.deepEqual(await E(E(userSeat).getExitSubscriber()).getUpdateSince(), { + updateCount: undefined, + value: undefined, + }); }); test('numWantsSatisfied as promise', async t => { @@ -1317,4 +1361,9 @@ test('numWantsSatisfied as promise', async t => { await zcfSeat.exit(); await outcome; + + t.deepEqual(await E(E(userSeat).getExitSubscriber()).getUpdateSince(), { + updateCount: undefined, + value: undefined, + }); });