diff --git a/packages/vats/package.json b/packages/vats/package.json index b38054b2d2b..38f233e962e 100644 --- a/packages/vats/package.json +++ b/packages/vats/package.json @@ -15,6 +15,7 @@ "build:boot-viz-sim-gov": "node src/authorityViz.js --sim-chain --gov >docs/boot-sim-gov.dot && dot -Tsvg docs/boot-sim-gov.dot >docs/boot-sim-gov.dot.svg", "build:restart-vats-proposal": "agoric run scripts/restart-vats.js", "build:add-STARS-proposal": "agoric run scripts/add-STARS.js", + "build:zcf-proposal": "agoric run scripts/replace-zoe.js", "prepack": "tsc --build jsconfig.build.json", "postpack": "git clean -f '*.d.ts*'", "test": "ava", diff --git a/packages/vats/scripts/replace-zoe.js b/packages/vats/scripts/replace-zoe.js new file mode 100644 index 00000000000..00fc1878bc6 --- /dev/null +++ b/packages/vats/scripts/replace-zoe.js @@ -0,0 +1,19 @@ +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: '../src/proposals/zcf-proposal.js', + getManifestCall: [ + 'getManifestForZoe', + { + zoeRef: publishRef(install('../src/vat-zoe.js')), + zcfRef: publishRef(install('../../zoe/src/contractFacet/vatRoot.js')), + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('replace-zcf', defaultProposalBuilder); +}; diff --git a/packages/vats/src/proposals/zcf-proposal.js b/packages/vats/src/proposals/zcf-proposal.js new file mode 100644 index 00000000000..1111551fc16 --- /dev/null +++ b/packages/vats/src/proposals/zcf-proposal.js @@ -0,0 +1,46 @@ +import { E } from '@endo/far'; + +/** + * @param { BootstrapPowers & { + * consume: { + * vatAdminSvc: VatAdminSve, + * vatStore: MapStore, + * } + * }} powers + * + * @param {object} options + * @param {{zoeRef: VatSourceRef, zcfRef: VatSourceRef}} options.options + */ +export const upgradeZcf = async ( + { consume: { vatAdminSvc, vatStore } }, + options, +) => { + const { zoeRef, zcfRef } = options.options; + + const zoeBundleCap = await E(vatAdminSvc).getBundleCap(zoeRef.bundleID); + console.log(`ZOE BUNDLE ID: `, zoeRef.bundleID); + + const { adminNode, root: zoeRoot } = await E(vatStore).get('zoe'); + + await E(adminNode).upgrade(zoeBundleCap, {}); + + const zoeConfigFacet = await E(zoeRoot).getZoeConfigFacet(); + await E(zoeConfigFacet).updateZcfBundleId(zcfRef.bundleID); + console.log(`ZCF BUNDLE ID: `, zcfRef.bundleID); +}; + +export const getManifestForZoe = (_powers, { zoeRef, zcfRef }) => ({ + manifest: { + [upgradeZcf.name]: { + consume: { + vatAdminSvc: 'vatAdminSvc', + vatStore: 'vatStore', + }, + produce: {}, + }, + }, + options: { + zoeRef, + zcfRef, + }, +}); diff --git a/packages/vats/test/bootstrapTests/drivers.js b/packages/vats/test/bootstrapTests/drivers.js index d5ffe41bac6..748c6eb22dd 100644 --- a/packages/vats/test/bootstrapTests/drivers.js +++ b/packages/vats/test/bootstrapTests/drivers.js @@ -5,6 +5,7 @@ import { SECONDS_PER_MINUTE } from '@agoric/inter-protocol/src/proposals/econ-be import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js'; import { slotToRemotable } from '@agoric/internal/src/storage-test-utils.js'; import { instanceNameFor } from '@agoric/inter-protocol/src/proposals/price-feed-proposal.js'; + import { boardSlottingMarshaller } from '../../tools/board-utils.js'; /** @@ -337,3 +338,90 @@ export const makeGovernanceDriver = async ( }, }; }; + +/** + * @param {import('./supports.js').SwingsetTestKit} testKit + */ +export const makeZoeDriver = async testKit => { + const { EV } = testKit.runUtils; + const zoe = await EV.vat('bootstrap').consumeItem('zoe'); + const chainStorage = await EV.vat('bootstrap').consumeItem('chainStorage'); + const storageNode = await EV(chainStorage).makeChildNode('prober-asid9a'); + let creatorFacet; + let adminFacet; + let brand; + const sub = (a, v) => { + return { brand: a.brand, value: a.value - v }; + }; + + return { + async instantiateProbeContract(probeContractBundle) { + const installation = await EV(zoe).install(probeContractBundle); + const startResults = await EV(zoe).startInstance( + installation, + undefined, + undefined, + { storageNode }, + 'probe', + ); + ({ creatorFacet, adminFacet } = startResults); + + const issuers = await EV(zoe).getIssuers(startResults.instance); + const brands = await EV(zoe).getBrands(startResults.instance); + brand = brands.Ducats; + return { creatorFacet, issuer: issuers.Ducats, brand }; + }, + async upgradeProbe(probeContractBundle) { + const fabricateBundleId = bundle => { + return `b1-${bundle.endoZipBase64Sha512}`; + }; + + await EV(adminFacet).upgradeContract( + fabricateBundleId(probeContractBundle), + ); + }, + + verifyRealloc() { + return EV(creatorFacet).getAllocation(); + }, + async probeReallocation(value, payment) { + const stagingInv = await EV(creatorFacet).makeProbeStagingInvitation(); + + const stagingSeat = await EV(zoe).offer( + stagingInv, + { give: { Ducats: value } }, + { Ducats: payment }, + ); + const helperPayments = await EV(stagingSeat).getPayouts(); + + const helperInv = await EV(creatorFacet).makeProbeHelperInvitation(); + const helperSeat = await EV(zoe).offer( + helperInv, + { give: { Ducats: sub(value, 1n) } }, + { Ducats: helperPayments.Ducats }, + ); + const internalPayments = await EV(helperSeat).getPayouts(); + + const internalInv = await EV(creatorFacet).makeProbeInternalInvitation(); + const internalSeat = await EV(zoe).offer( + internalInv, + { give: { Ducats: sub(value, 2n) } }, + { Ducats: internalPayments.Ducats }, + ); + const leftoverPayments = await EV(internalSeat).getPayouts(); + + return { + stagingResult: await EV(stagingSeat).getOfferResult(), + helperResult: await EV(helperSeat).getOfferResult(), + internalResult: await EV(internalSeat).getOfferResult(), + leftoverPayments, + }; + }, + async faucet() { + const faucetInv = await EV(creatorFacet).makeFaucetInvitation(); + const seat = await EV(zoe).offer(faucetInv); + + return EV(seat).getPayout('Ducats'); + }, + }; +}; diff --git a/packages/vats/test/bootstrapTests/supports.js b/packages/vats/test/bootstrapTests/supports.js index b01549585d5..ee9dc71df36 100644 --- a/packages/vats/test/bootstrapTests/supports.js +++ b/packages/vats/test/bootstrapTests/supports.js @@ -1,6 +1,7 @@ // @ts-check /* global process */ -import * as fsAmbient from 'fs'; + +import { promises as fsAmbientPromises } from 'fs'; import { resolve as importMetaResolve } from 'import-meta-resolve'; import { basename } from 'path'; import { inspect } from 'util'; @@ -17,14 +18,22 @@ import { loadSwingsetConfigFile } from '@agoric/swingset-vat'; import { E } from '@endo/eventual-send'; import { makeQueue } from '@endo/stream'; import { TimeMath } from '@agoric/time'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; + +import * as processAmbient from 'child_process'; import { boardSlottingMarshaller, + makeAgoricNamesRemotesFromFakeStorage, slotToBoardRemote, } from '../../tools/board-utils.js'; +import { makeWalletFactoryDriver, makeZoeDriver } from './drivers.js'; // to retain for ESlint, used by typedef E; +// main/production config doesn't have initialPrice, upon which 'open vaults' depends +const PLATFORM_CONFIG = '@agoric/vats/decentral-itest-vaults-config.json'; + const sink = () => {}; const trace = makeTracer('BSTSupport', false); @@ -199,7 +208,7 @@ export const getNodeTestVaultsConfig = async ( config.defaultManagerType = 'local'; // speed up build (60s down to 10s in testing) config.bundleCachePath = bundleDir; - await fsAmbient.promises.mkdir(bundleDir, { recursive: true }); + await fsAmbientPromises.mkdir(bundleDir, { recursive: true }); if (config.coreProposals) { // remove Pegasus because it relies on IBC to Golang that isn't running @@ -209,7 +218,7 @@ export const getNodeTestVaultsConfig = async ( } const testConfigPath = `${bundleDir}/${basename(specifier)}`; - await fsAmbient.promises.writeFile( + await fsAmbientPromises.writeFile( testConfigPath, JSON.stringify(config), 'utf-8', @@ -434,7 +443,7 @@ export const makeSwingsetTestKit = async ( const buildProposal = makeProposalExtractor({ childProcess: childProcessAmbient, - fs: fsAmbient.promises, + fs: fsAmbientPromises, }); console.timeEnd('makeSwingsetTestKit'); @@ -493,4 +502,94 @@ export const makeSwingsetTestKit = async ( timer, }; }; + /** @typedef {Awaited>} SwingsetTestKit */ + +export const makeTestContext = async t => { + console.time('DefaultTestContext'); + /** @type {SwingsetTestKit} */ + const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/vaults', { + configSpecifier: PLATFORM_CONFIG, + }); + + const { runUtils, storage } = swingsetTestKit; + console.timeLog('DefaultTestContext', 'swingsetTestKit'); + const { EV } = runUtils; + + // Wait for ATOM to make it into agoricNames + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); + console.timeLog('DefaultTestContext', 'vaultFactoryKit'); + + await eventLoopIteration(); + + // has to be late enough for agoricNames data to have been published + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage( + swingsetTestKit.storage, + ); + agoricNamesRemotes.brand.ATOM || Fail`ATOM brand not yet defined`; + console.timeLog('DefaultTestContext', 'agoricNamesRemotes'); + + const walletFactoryDriver = await makeWalletFactoryDriver( + runUtils, + storage, + agoricNamesRemotes, + ); + console.timeLog('DefaultTestContext', 'walletFactoryDriver'); + + console.timeEnd('DefaultTestContext'); + + const buildProposal = makeProposalExtractor({ + childProcess: processAmbient, + fs: fsAmbientPromises, + }); + + return { + ...swingsetTestKit, + agoricNamesRemotes, + walletFactoryDriver, + buildProposal, + }; +}; + +export const makeZoeTestContext = async t => { + console.time('DefaultTestContext'); + /** @type {SwingsetTestKit} */ + const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/zoe', { + configSpecifier: '@agoric/vats/decentral-demo-config.json', + }); + + const { controller, runUtils } = swingsetTestKit; + console.timeLog('DefaultTestContext', 'swingsetTestKit'); + const { EV } = runUtils; + + await eventLoopIteration(); + + // We don't need vaults, but this gets the brand, which is checked somewhere + // Wait for ATOM to make it into agoricNames + await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); + console.timeLog('DefaultTestContext', 'vaultFactoryKit'); + + // has to be late enough for agoricNames data to have been published + const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage( + swingsetTestKit.storage, + ); + console.timeLog('DefaultTestContext', 'agoricNamesRemotes'); + + const zoeDriver = await makeZoeDriver(swingsetTestKit); + console.timeLog('DefaultTestContext', 'walletFactoryDriver'); + + console.timeEnd('DefaultTestContext'); + + const buildProposal = makeProposalExtractor({ + childProcess: processAmbient, + fs: fsAmbientPromises, + }); + + return { + ...swingsetTestKit, + controller, + agoricNamesRemotes, + zoeDriver, + buildProposal, + }; +}; diff --git a/packages/vats/test/bootstrapTests/test-vats-restart.js b/packages/vats/test/bootstrapTests/test-vats-restart.js index d575a041df0..93431478f9f 100644 --- a/packages/vats/test/bootstrapTests/test-vats-restart.js +++ b/packages/vats/test/bootstrapTests/test-vats-restart.js @@ -2,12 +2,10 @@ /** @file Bootstrap test of restarting (almost) all vats */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { Fail } from '@agoric/assert'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; -import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; -import { makeAgoricNamesRemotesFromFakeStorage } from '../../tools/board-utils.js'; -import { makeWalletFactoryDriver } from './drivers.js'; -import { makeSwingsetTestKit } from './supports.js'; +import { makeTestContext } from './supports.js'; + +/** @file Bootstrap test of restarting (almost) all vats */ /** * @type {import('ava').TestFn< @@ -16,51 +14,9 @@ import { makeSwingsetTestKit } from './supports.js'; */ const test = anyTest; -// main/production config doesn't have initialPrice, upon which 'open vaults' depends -const PLATFORM_CONFIG = '@agoric/vats/decentral-itest-vaults-config.json'; - // presently all these tests use one collateral manager const collateralBrandKey = 'ATOM'; -const makeTestContext = async t => { - console.time('DefaultTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/vaults', { - configSpecifier: PLATFORM_CONFIG, - }); - - const { runUtils, storage } = swingsetTestKit; - console.timeLog('DefaultTestContext', 'swingsetTestKit'); - const { EV } = runUtils; - - // Wait for ATOM to make it into agoricNames - await EV.vat('bootstrap').consumeItem('vaultFactoryKit'); - console.timeLog('DefaultTestContext', 'vaultFactoryKit'); - - await eventLoopIteration(); - - // has to be late enough for agoricNames data to have been published - const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage( - swingsetTestKit.storage, - ); - agoricNamesRemotes.brand.ATOM || Fail`ATOM brand not yet defined`; - console.timeLog('DefaultTestContext', 'agoricNamesRemotes'); - - const walletFactoryDriver = await makeWalletFactoryDriver( - runUtils, - storage, - agoricNamesRemotes, - ); - console.timeLog('DefaultTestContext', 'walletFactoryDriver'); - - console.timeEnd('DefaultTestContext'); - - return { - ...swingsetTestKit, - agoricNamesRemotes, - walletFactoryDriver, - }; -}; - test.before(async t => { t.context = await makeTestContext(t); }); diff --git a/packages/vats/test/bootstrapTests/test-zcf-upgrade.js b/packages/vats/test/bootstrapTests/test-zcf-upgrade.js new file mode 100644 index 00000000000..9ef41fd6930 --- /dev/null +++ b/packages/vats/test/bootstrapTests/test-zcf-upgrade.js @@ -0,0 +1,107 @@ +// @ts-check + +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import bundleSource from '@endo/bundle-source'; + +import path from 'path'; +import { makeZoeTestContext } from './supports.js'; + +const filename = new URL(import.meta.url).pathname; +const dirname = path.dirname(filename); + +const ZCF_PROBE_SRC = './zcfProbe.js'; + +/** + * @file Bootstrap test of upgrading ZCF to support atomicRearrange internally. + * + * The goal is to tell Zoe about a new version of ZCF that it should use + * when starting new contracts. Zoe wasn't previously configurable for that, so + * a prerequisite was to upgrade Zoe to a version that could have its ZCF + * updated. To test that we install a contract that can detect the variation + * among zcf versions, and run it before, in the middle and after the upgrades. + * + * 0. add a contract that can report on the state of ZCF's support for + * different versions of reallocation: staging, helper, and internal. + * 1. put new Zoe & ZCF bundles on chain + * 2. upgrade Zoe; return a new facet that supports ZCF update + * 3. tell Zoe to use new ZCF + * 4. restart the new contract; verify that the behavior is unchanged. + * 5. null upgrade the contract; verify that zcf supports internal rearrange. + * 6. [optional] fully upgrade the contract; verify that it works + */ + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +test.before(async t => { + t.context = await makeZoeTestContext(t); +}); +test.after.always(t => t.context.shutdown?.()); + +test('run restart-vats proposal', async t => { + const { controller, buildProposal, zoeDriver } = t.context; + const { EV } = t.context.runUtils; + + const buildAndExecuteProposal = async packageSpec => { + const proposal = await buildProposal(packageSpec); + + for await (const bundle of proposal.bundles) { + await controller.validateAndInstallBundle(bundle); + } + + t.log('installed', proposal.bundles.length, 'bundles'); + + t.log('launching proposal'); + const bridgeMessage = { + type: 'CORE_EVAL', + evals: proposal.evals, + }; + + t.log({ bridgeMessage }); + /** @type {ERef} */ + const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem( + 'coreEvalBridgeHandler', + ); + await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage); + }; + const source = `${dirname}/${ZCF_PROBE_SRC}`; + + const zcfProbeBundle = await bundleSource(source); + // uncomment and add `import fs from "fs";` to generate a bundle of the prober contract + // fs.writeFileSync('bundles/prober-contract-bundle.json', JSON.stringify(zcfProbeBundle)); + + const brandRecord = await zoeDriver.instantiateProbeContract(zcfProbeBundle); + const { brand, issuer } = brandRecord; + const ducatAmountRecord = v => ({ Ducats: { brand, value: v } }); + + t.deepEqual(await zoeDriver.verifyRealloc(), {}); + + const ducats = await zoeDriver.faucet(); + const initialAmount = await EV(issuer).getAmountOf(ducats); + + const beforeResult = await zoeDriver.probeReallocation(initialAmount, ducats); + t.true(beforeResult.stagingResult); + t.true(beforeResult.helperResult); + // In this version of the test, we're upgrading from new ZCF to new ZCF + t.true(beforeResult.internalResult); + t.deepEqual(await zoeDriver.verifyRealloc(), ducatAmountRecord(3n)); + + t.log('building proposal'); + // /////// Upgrading //////////////////////////////// + const zcfPackageSpec = { + package: 'vats', + packageScriptName: 'build:zcf-proposal', + }; + await buildAndExecuteProposal(zcfPackageSpec); + + t.log('upgrade zoe&zcf proposal executed'); + zoeDriver.upgradeProbe(zcfProbeBundle); + const nextDucats = beforeResult.leftoverPayments.Ducats; + const nextAmount = await EV(issuer).getAmountOf(nextDucats); + + const afterResult = await zoeDriver.probeReallocation(nextAmount, nextDucats); + t.true(afterResult.stagingResult); + t.true(afterResult.helperResult); + t.true(afterResult.internalResult); + t.deepEqual(await zoeDriver.verifyRealloc(), ducatAmountRecord(6n)); +}); diff --git a/packages/vats/test/bootstrapTests/zcfProbe.js b/packages/vats/test/bootstrapTests/zcfProbe.js new file mode 100644 index 00000000000..11c6dd5d6a0 --- /dev/null +++ b/packages/vats/test/bootstrapTests/zcfProbe.js @@ -0,0 +1,138 @@ +import { makeTracer } from '@agoric/internal'; +import { E } from '@endo/far'; +import { + atomicRearrange, + provideAll, +} from '@agoric/zoe/src/contractSupport/index.js'; +import { M, prepareExoClass, provide } from '@agoric/vat-data'; +import { AmountMath } from '@agoric/ertp'; + +const trace = makeTracer('ZCF Probe'); + +const ZcfProbeI = M.interface('ZCF Probe', { + makeProbeHelperInvitation: M.call().returns(M.promise()), + makeProbeInternalInvitation: M.call().returns(M.promise()), + makeProbeStagingInvitation: M.call().returns(M.promise()), + getAllocation: M.call().returns(M.any()), + makeFaucetInvitation: M.call().returns(M.promise()), +}); + +/** + * @param {ZCF} zcf + * @param {{storageNode: StorageNode}} privateArgs + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const prepare = async (zcf, privateArgs, baggage) => { + const { probeMint } = await provideAll(baggage, { + probeMint: () => zcf.makeZCFMint('Ducats'), + }); + + const { storageNode } = privateArgs; + const makeZcfProbe = await prepareExoClass( + baggage, + 'zcfProbe', + ZcfProbeI, + () => ({ + stashSeat: zcf.makeEmptySeatKit().zcfSeat, + probeMint: null, + }), + { + makeProbeHelperInvitation() { + const { stashSeat } = this.state; + + const probeHelper = seat => { + trace('ProbeHelper'); + const originalAlloc = seat.getCurrentAllocation().Ducats; + const one = AmountMath.make(originalAlloc.brand, 1n); + let result; + try { + atomicRearrange(zcf, harden([[seat, stashSeat, { Ducats: one }]])); + result = true; + } catch (e) { + result = false; + } + + seat.exit(); + return result; + }; + + return zcf.makeInvitation(probeHelper, 'probe helper'); + }, + makeProbeInternalInvitation() { + const { stashSeat } = this.state; + const probeInternal = seat => { + trace('ProbeIntrinsics'); + const originalAlloc = seat.getCurrentAllocation().Ducats; + const one = AmountMath.make(originalAlloc.brand, 1n); + let result; + try { + zcf.atomicRearrange(harden([[seat, stashSeat, { Ducats: one }]])); + result = true; + } catch (e) { + result = false; + } + + seat.clear(); + seat.exit(); + + trace('Intrinsics', result); + // write to vstorage so a test can detect it. + void E(storageNode).setValue(`${result}`); + + return result; + }; + + return zcf.makeInvitation(probeInternal, 'probe intrinsic'); + }, + makeProbeStagingInvitation() { + const { stashSeat } = this.state; + + const probeStaging = seat => { + trace('ProbeStaging'); + + const originalAlloc = seat.getCurrentAllocation().Ducats; + const one = AmountMath.make(originalAlloc.brand, 1n); + let result; + try { + stashSeat.incrementBy(seat.decrementBy({ Ducats: one })); + zcf.reallocate(seat, stashSeat); + result = true; + } catch (e) { + seat.clear(); + stashSeat.clear(); + result = false; + } + + seat.exit(); + return result; + }; + + return zcf.makeInvitation(probeStaging, 'probe staging'); + }, + getAllocation() { + const { stashSeat } = this.state; + trace('getAllocation'); + + return stashSeat.getCurrentAllocation(); + }, + makeFaucetInvitation() { + return zcf.makeInvitation(async seat => { + trace('faucet'); + const { brand } = await probeMint.getIssuerRecord(); + + await probeMint.mintGains( + { Ducats: AmountMath.make(brand, 16n) }, + seat, + ); + seat.exit(); + return 'minted 16n Ducats'; + }, 'faucet'); + }, + }, + ); + + const probe = await provide(baggage, 'probe', () => makeZcfProbe()); + return harden({ + creatorFacet: probe, + }); +};