From 63fc6fafc7d349ed0cb132833b02cbdc903c65b9 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Fri, 16 Jun 2023 16:16:02 -0700 Subject: [PATCH] test: a test framework for verifying upgrade of Zoe and ZCF It starts from the currently installed version of Zoe and ZCF. First it verifies that reallocation via staging and via the helper work and that the version internal to ZCF is not present. It then upgrades Zoe and ZCF as necessary to introduce the new behavior. Finally, it re-runs the initial verification to show that the ZCF internal version works. This currently fails on the first step since it runs in a state in which the new Zoe/ZCF code has already replaced the old. --- packages/vats/package.json | 1 + packages/vats/scripts/replace-zoe.js | 19 ++ packages/vats/src/proposals/zcf-proposal.js | 44 ++++ packages/vats/src/vat-zoe.js | 7 +- packages/vats/test/bootstrapTests/drivers.js | 71 ++++++ packages/vats/test/bootstrapTests/supports.js | 206 +++++++++++++++++- .../test/bootstrapTests/test-vats-restart.js | 152 +------------ .../test/bootstrapTests/test-zcf-upgrade.js | 94 ++++++++ packages/vats/test/bootstrapTests/zcfProbe.js | 129 +++++++++++ packages/zoe/src/typeGuards.js | 1 + packages/zoe/src/zoeService/zoe.js | 29 ++- 11 files changed, 597 insertions(+), 156 deletions(-) create mode 100644 packages/vats/scripts/replace-zoe.js create mode 100644 packages/vats/src/proposals/zcf-proposal.js create mode 100644 packages/vats/test/bootstrapTests/test-zcf-upgrade.js create mode 100644 packages/vats/test/bootstrapTests/zcfProbe.js diff --git a/packages/vats/package.json b/packages/vats/package.json index ad128928217..7ff6ac4fa9f 100644 --- a/packages/vats/package.json +++ b/packages/vats/package.json @@ -14,6 +14,7 @@ "build:boot-viz-sim": "node src/authorityViz.js --sim-chain >docs/boot-sim.dot && dot -Tsvg docs/boot-sim.dot >docs/boot-sim.dot.svg", "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: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..3a245895ac7 --- /dev/null +++ b/packages/vats/src/proposals/zcf-proposal.js @@ -0,0 +1,44 @@ +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); + + 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); +}; + +export const getManifestForZoe = (_powers, { zoeRef, zcfRef }) => ({ + manifest: { + [upgradeZcf.name]: { + consume: { + vatAdminSvc: 'vatAdminSvc', + vatStore: 'vatStore', + }, + produce: {}, + }, + }, + options: { + zoeRef, + zcfRef, + }, +}); diff --git a/packages/vats/src/vat-zoe.js b/packages/vats/src/vat-zoe.js index cb9973e628c..7591b2837b5 100644 --- a/packages/vats/src/vat-zoe.js +++ b/packages/vats/src/vat-zoe.js @@ -6,15 +6,17 @@ const BUILD_PARAMS_KEY = 'buildZoeParams'; export function buildRootObject(vatPowers, _vatParams, zoeBaggage) { const shutdownZoeVat = vatPowers.exitVatWithFailure; + let zoeConfigFacet; + if (zoeBaggage.has(BUILD_PARAMS_KEY)) { const { feeIssuerConfig, zcfSpec } = zoeBaggage.get(BUILD_PARAMS_KEY); - makeDurableZoeKit({ + ({ zoeConfigFacet } = makeDurableZoeKit({ // For now Zoe will rewire vatAdminSvc on its own shutdownZoeVat, feeIssuerConfig, zcfSpec, zoeBaggage, - }); + })); } return Far('root', { @@ -44,5 +46,6 @@ export function buildRootObject(vatPowers, _vatParams, zoeBaggage) { feeMintAccess, }); }, + getZoeConfigFacet: () => zoeConfigFacet, }); } diff --git a/packages/vats/test/bootstrapTests/drivers.js b/packages/vats/test/bootstrapTests/drivers.js index aa2824d8519..a1a4dc1a86c 100644 --- a/packages/vats/test/bootstrapTests/drivers.js +++ b/packages/vats/test/bootstrapTests/drivers.js @@ -4,6 +4,7 @@ import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { SECONDS_PER_MINUTE } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; import { unmarshalFromVstorage } from '@agoric/internal/src/lib-chainStorage.js'; import { slotToRemotable } from '@agoric/internal/src/storage-test-utils.js'; + import { boardSlottingMarshaller } from '../../tools/board-utils.js'; /** @@ -339,3 +340,73 @@ 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'); + let creatorFacet; + 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); + ({ creatorFacet } = 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 }; + }, + + verifyRealloc() { + const alloc = EV(creatorFacet).getAllocation(); + return alloc; + }, + 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 2ad85a28a13..514755aaad9 100644 --- a/packages/vats/test/bootstrapTests/supports.js +++ b/packages/vats/test/bootstrapTests/supports.js @@ -1,5 +1,8 @@ // @ts-check -import { promises as fs } from 'fs'; + +/* global process */ + +import { promises as fsAmbientPromises } from 'fs'; import { resolve as importMetaResolve } from 'import-meta-resolve'; import { basename } from 'path'; import { inspect } from 'util'; @@ -16,14 +19,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); @@ -191,7 +202,7 @@ export const getNodeTestVaultsConfig = async ( config.defaultManagerType = 'local'; // speed up build (60s down to 10s in testing) config.bundleCachePath = bundleDir; - await fs.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 @@ -201,7 +212,11 @@ export const getNodeTestVaultsConfig = async ( } const testConfigPath = `${bundleDir}/${basename(specifier)}`; - await fs.writeFile(testConfigPath, JSON.stringify(config), 'utf-8'); + await fsAmbientPromises.writeFile( + testConfigPath, + JSON.stringify(config), + 'utf-8', + ); return testConfigPath; }; @@ -384,4 +399,189 @@ export const makeSwingsetTestKit = async ( timer, }; }; + +/** + * @param {object} powers + * @param {Pick} powers.childProcess + * @param {typeof import('node:fs/promises')} powers.fs + */ +const makeProposalExtractor = ({ childProcess, fs }) => { + const getPkgPath = (pkg, fileName = '') => + new URL(`../../../${pkg}/${fileName}`, import.meta.url).pathname; + + const runPackageScript = async (pkg, name, env) => { + console.warn(pkg, 'running package script:', name); + const pkgPath = getPkgPath(pkg); + return childProcess.execFileSync('yarn', ['run', name], { + cwd: pkgPath, + env, + }); + }; + + const loadJSON = async filePath => + harden(JSON.parse(await fs.readFile(filePath, 'utf8'))); + + // XXX parses the output to find the files but could write them to a path that can be traversed + /** @param {string} txt */ + const parseProposalParts = txt => { + const evals = [ + ...txt.matchAll(/swingset-core-eval (?\S+) (?