From 1618fb4c61b8324e8437278e0647191d2a0b0600 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 22 Jun 2023 09:29:33 -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. Variants of this test will run on master without this PR to verify that upgrading to the new Zoe/ZCF works, and in deployment/upgrade-test to show that the upgrade to the new zoe & zcf will succeed. --- packages/vats/package.json | 1 + packages/vats/scripts/replace-zoe.js | 19 ++ packages/vats/src/proposals/zcf-proposal.js | 44 ++++ packages/vats/test/bootstrapTests/drivers.js | 81 +++++++ packages/vats/test/bootstrapTests/supports.js | 204 +++++++++++++++++- .../test/bootstrapTests/test-vats-restart.js | 152 +------------ .../test/bootstrapTests/test-zcf-upgrade.js | 104 +++++++++ packages/vats/test/bootstrapTests/zcfProbe.js | 132 ++++++++++++ 8 files changed, 584 insertions(+), 153 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/test/bootstrapTests/drivers.js b/packages/vats/test/bootstrapTests/drivers.js index aa2824d8519..0c54a0d422b 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,83 @@ 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 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); + ({ 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() { + 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..093d1592473 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,187 @@ 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+) (?