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..e3fcb1868df --- /dev/null +++ b/packages/vats/src/proposals/zcf-proposal.js @@ -0,0 +1,45 @@ +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..4cede9cc226 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'; /** @@ -205,8 +206,10 @@ export const makePriceFeedDriver = async ( }; }; +/** @typedef {Awaited>} SwingsetTestKit */ + /** - * @param {import('./supports.js').SwingsetTestKit} testKit + * @param {SwingsetTestKit} testKit * @param {import('../../tools/board-utils.js').AgoricNamesRemotes} agoricNamesRemotes * @param {Awaited>} walletFactoryDriver * @param {string[]} committeeAddresses @@ -337,3 +340,87 @@ export const makeGovernanceDriver = async ( }, }; }; + +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..21cd6c1ec8b 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'; @@ -199,7 +200,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 +210,7 @@ export const getNodeTestVaultsConfig = async ( } const testConfigPath = `${bundleDir}/${basename(specifier)}`; - await fsAmbient.promises.writeFile( + await fsAmbientPromises.writeFile( testConfigPath, JSON.stringify(config), 'utf-8', @@ -222,7 +223,7 @@ export const getNodeTestVaultsConfig = async ( * @param {Pick} powers.childProcess * @param {typeof import('node:fs/promises')} powers.fs */ -const makeProposalExtractor = ({ childProcess, fs }) => { +export const makeProposalExtractor = ({ childProcess, fs }) => { const getPkgPath = (pkg, fileName = '') => new URL(`../../../${pkg}/${fileName}`, import.meta.url).pathname; @@ -309,6 +310,7 @@ const makeProposalExtractor = ({ childProcess, fs }) => { }; return buildAndExtract; }; +harden(makeProposalExtractor); /** * Start a SwingSet kernel to be shared across all tests. By default Ava tests @@ -434,7 +436,7 @@ export const makeSwingsetTestKit = async ( const buildProposal = makeProposalExtractor({ childProcess: childProcessAmbient, - fs: fsAmbient.promises, + fs: fsAmbientPromises, }); console.timeEnd('makeSwingsetTestKit'); @@ -493,4 +495,3 @@ export const makeSwingsetTestKit = async ( timer, }; }; -/** @typedef {Awaited>} SwingsetTestKit */ diff --git a/packages/vats/test/bootstrapTests/test-vats-restart.js b/packages/vats/test/bootstrapTests/test-vats-restart.js index d575a041df0..d1358bef893 100644 --- a/packages/vats/test/bootstrapTests/test-vats-restart.js +++ b/packages/vats/test/bootstrapTests/test-vats-restart.js @@ -1,29 +1,27 @@ // @ts-check /** @file Bootstrap test of restarting (almost) all vats */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; + +import processAmbient from 'child_process'; +import { promises as fsAmbientPromises } from 'fs'; -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 { makeProposalExtractor, makeSwingsetTestKit } from './supports.js'; -/** - * @type {import('ava').TestFn< - * Awaited> - * >} - */ -const test = anyTest; +const { Fail } = assert; + +/** @file Bootstrap test of restarting (almost) all vats */ // main/production config doesn't have initialPrice, upon which 'open vaults' depends const PLATFORM_CONFIG = '@agoric/vats/decentral-itest-vaults-config.json'; +/** @typedef {Awaited>} SwingsetTestKit */ -// presently all these tests use one collateral manager -const collateralBrandKey = 'ATOM'; - -const makeTestContext = async t => { +export const makeTestContext = async t => { console.time('DefaultTestContext'); + /** @type {SwingsetTestKit} */ const swingsetTestKit = await makeSwingsetTestKit(t, 'bundles/vaults', { configSpecifier: PLATFORM_CONFIG, }); @@ -54,12 +52,27 @@ const makeTestContext = async t => { console.timeEnd('DefaultTestContext'); + const buildProposal = makeProposalExtractor({ + childProcess: processAmbient, + fs: fsAmbientPromises, + }); + return { ...swingsetTestKit, agoricNamesRemotes, walletFactoryDriver, + buildProposal, }; }; +/** + * @type {import('ava').TestFn< + * Awaited> + * >} + */ +const test = anyTest; + +// presently all these tests use one collateral manager +const collateralBrandKey = 'ATOM'; 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..bfea74c9452 --- /dev/null +++ b/packages/vats/test/bootstrapTests/test-zcf-upgrade.js @@ -0,0 +1,168 @@ +// @ts-check + +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import bundleSource from '@endo/bundle-source'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; + +import path from 'path'; +import processAmbient from 'child_process'; +import { promises as fsAmbientPromises } from 'fs'; + +import { makeAgoricNamesRemotesFromFakeStorage } from '../../tools/board-utils.js'; +import { makeZoeDriver } from './drivers.js'; +import { makeProposalExtractor, makeSwingsetTestKit } from './supports.js'; + +const filename = new URL(import.meta.url).pathname; +const dirname = path.dirname(filename); + +const ZCF_PROBE_SRC = './zcfProbe.js'; + +/** @typedef {Awaited>} SwingsetTestKit */ + +/** + * @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 + */ + +export const makeZoeTestContext = async t => { + console.time('ZoeTestContext'); + /** @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, + }; +}; + +/** + * @type {import('ava').TestFn< + * Awaited> + * >} + */ +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); + await controller.validateAndInstallBundle(zcfProbeBundle); + // This test self-sufficiently builds all the artifacts it needs. The test in + // .../packages/deployment/upgradeTest/upgradeTest-scripts/agoric-upgrade-11/zoe-upgrade/ + // needs a bundled copy of ./zcfProbe.js as of the final commit that will be + // installed on-chain. Uncomment the following line and add + // `import fs from "fs";` to generate a bundle of the 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..04d47bb2866 --- /dev/null +++ b/packages/vats/test/bootstrapTests/zcfProbe.js @@ -0,0 +1,146 @@ +import '@agoric/zoe/exported.js'; + +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()), +}); + +// /** @type {ContractMeta} */ +// export const meta = { upgradability: 'canUpgrade' }; +// harden(meta); + +/** + * @param {ZCF} zcf + * @param {{ storageNode: StorageNode }} privateArgs + * @param {import('@agoric/vat-data').Baggage} baggage + */ +export const start = async (zcf, privateArgs, baggage) => { + const { probeMint } = await provideAll(baggage, { + probeMint: () => zcf.makeZCFMint('Ducats'), + }); + + const storageNode = privateArgs?.storageNode; + 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. + if (storageNode) { + 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, + }); +};