From cc5512aac07341924c6b1925947a8ad66d59d7da Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Wed, 8 Jan 2025 14:58:41 -0800 Subject: [PATCH] test: test that APIs can change on upgrade --- packages/boot/package.json | 1 + .../test/bootstrapTests/governedContract.js | 85 ++++++++ .../test/bootstrapTests/governedContract2.js | 90 +++++++++ .../updateUpgradedVaultParams.test.ts | 2 +- .../test/bootstrapTests/upgradeAPI.test.ts | 183 ++++++++++++++++++ 5 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 packages/boot/test/bootstrapTests/governedContract.js create mode 100644 packages/boot/test/bootstrapTests/governedContract2.js create mode 100644 packages/boot/test/bootstrapTests/upgradeAPI.test.ts diff --git a/packages/boot/package.json b/packages/boot/package.json index 1226c3b8065e..cd69c6273a2a 100644 --- a/packages/boot/package.json +++ b/packages/boot/package.json @@ -26,6 +26,7 @@ "@agoric/cosmic-swingset": "^0.41.3", "@agoric/ertp": "^0.16.2", "@agoric/fast-usdc": "0.1.0", + "@agoric/governance": "^0.10.3", "@agoric/inter-protocol": "^0.16.1", "@agoric/internal": "^0.3.2", "@agoric/kmarshal": "^0.1.0", diff --git a/packages/boot/test/bootstrapTests/governedContract.js b/packages/boot/test/bootstrapTests/governedContract.js new file mode 100644 index 000000000000..a0ef04b86fb5 --- /dev/null +++ b/packages/boot/test/bootstrapTests/governedContract.js @@ -0,0 +1,85 @@ +import { + CONTRACT_ELECTORATE, + ParamTypes, + handleParamGovernance, +} from '@agoric/governance'; +import { prepareExoClassKit, provide } from '@agoric/vat-data'; + +/** + * @import {GovernanceTerms} from '@agoric/governance/src/types.js'; + * @import {Baggage} from '@agoric/vat-data'; + */ + +const MALLEABLE_NUMBER = 'MalleableNumber'; + +const makeTerms = (number, invitationAmount) => { + return harden({ + governedParams: { + [MALLEABLE_NUMBER]: { type: ParamTypes.NAT, value: number }, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: invitationAmount, + }, + }, + }); +}; + +/** + * + * @param {ZCF< + * GovernanceTerms<{ + * MalleableNumber: 'nat', + * }>>} zcf + * @param {{initialPoserInvitation: Invitation}} privateArgs + * @param {Baggage} baggage + */ +const start = async (zcf, privateArgs, baggage) => { + const { makeDurableGovernorFacet, params } = await handleParamGovernance( + zcf, + privateArgs.initialPoserInvitation, + { + [MALLEABLE_NUMBER]: ParamTypes.NAT, + }, + ); + + const makeGoverned = prepareExoClassKit( + baggage, + 'governed Public', + undefined, + () => + harden({ + governanceAPICalled: 0, + }), + { + public: { + getNum() { + return params.getMalleableNumber(); + }, + getApiCalled() { + const { governanceAPICalled } = this.state; + return governanceAPICalled; + }, + }, + creator: {}, + governed: { + add1() { + const { state } = this; + state.governanceAPICalled += 1; + }, + }, + }, + ); + const facets = provide(baggage, 'theContract', () => makeGoverned()); + + const { governorFacet } = makeDurableGovernorFacet(baggage, facets.creator, { + add1: () => facets.governed.add1(), + }); + + return { publicFacet: facets.public, creatorFacet: governorFacet }; +}; + +harden(start); +harden(MALLEABLE_NUMBER); +harden(makeTerms); + +export { start, MALLEABLE_NUMBER, makeTerms }; diff --git a/packages/boot/test/bootstrapTests/governedContract2.js b/packages/boot/test/bootstrapTests/governedContract2.js new file mode 100644 index 000000000000..5d0013d3ad8c --- /dev/null +++ b/packages/boot/test/bootstrapTests/governedContract2.js @@ -0,0 +1,90 @@ +import { + CONTRACT_ELECTORATE, + ParamTypes, + handleParamGovernance, +} from '@agoric/governance'; +import { prepareExoClassKit, provide } from '@agoric/vat-data'; + +/** + * @import {GovernanceTerms} from '@agoric/governance/src/types.js'; + * @import {Baggage} from '@agoric/vat-data'; + */ + +const MALLEABLE_NUMBER = 'MalleableNumber'; + +const makeTerms = (number, invitationAmount) => { + return harden({ + governedParams: { + [MALLEABLE_NUMBER]: { type: ParamTypes.NAT, value: number }, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: invitationAmount, + }, + }, + }); +}; + +/** + * + * @param {ZCF< + * GovernanceTerms<{ + * MalleableNumber: 'nat', + * }>>} zcf + * @param {{initialPoserInvitation: Invitation}} privateArgs + * @param {Baggage} baggage + */ +const start = async (zcf, privateArgs, baggage) => { + const { makeDurableGovernorFacet, params } = await handleParamGovernance( + zcf, + privateArgs.initialPoserInvitation, + { + [MALLEABLE_NUMBER]: ParamTypes.NAT, + }, + ); + + const makeGoverned = prepareExoClassKit( + baggage, + 'governed Public', + undefined, + () => + harden({ + governanceAPICalled: 0, + }), + { + public: { + getNum() { + return params.getMalleableNumber(); + }, + getApiCalled() { + const { governanceAPICalled } = this.state; + return governanceAPICalled; + }, + }, + creator: {}, + governed: { + add1() { + const { state } = this; + state.governanceAPICalled += 1; + }, + add2() { + const { state } = this; + state.governanceAPICalled += 2; + }, + }, + }, + ); + const facets = provide(baggage, 'theContract', () => makeGoverned()); + + const { governorFacet } = makeDurableGovernorFacet(baggage, facets.creator, { + add1: () => facets.governed.add1(), + add2: () => facets.governed.add2(), + }); + + return { publicFacet: facets.public, creatorFacet: governorFacet }; +}; + +harden(start); +harden(MALLEABLE_NUMBER); +harden(makeTerms); + +export { start, MALLEABLE_NUMBER, makeTerms }; diff --git a/packages/boot/test/bootstrapTests/updateUpgradedVaultParams.test.ts b/packages/boot/test/bootstrapTests/updateUpgradedVaultParams.test.ts index 4f97e7ae9097..37edf6fcb39a 100644 --- a/packages/boot/test/bootstrapTests/updateUpgradedVaultParams.test.ts +++ b/packages/boot/test/bootstrapTests/updateUpgradedVaultParams.test.ts @@ -3,7 +3,7 @@ * We change a parameter so that provideParamGovernance() is called once, and * paramGoverance has been set. Then upgrade vaultFactory, so any ephemeral * objects from the contract held by the governor are gone, then try to change - * param again, to show that the bug is fixedd. + * param again, to show that the bug is fixed. */ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; diff --git a/packages/boot/test/bootstrapTests/upgradeAPI.test.ts b/packages/boot/test/bootstrapTests/upgradeAPI.test.ts new file mode 100644 index 000000000000..27ad2c73ab1b --- /dev/null +++ b/packages/boot/test/bootstrapTests/upgradeAPI.test.ts @@ -0,0 +1,183 @@ +import { test as anyTest } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; +import type { TestFn } from 'ava'; +import path from 'path'; +import bundleSource from '@endo/bundle-source'; +import { CONTRACT_ELECTORATE, ParamTypes } from '@agoric/governance'; +import { MALLEABLE_NUMBER } from '@agoric/governance/test/swingsetTests/contractGovernor/governedContract.js'; +import { makeSwingsetTestKit } from '../../tools/supports.js'; + +const dirname = path.dirname(new URL(import.meta.url).pathname); + +const GOVERNED_CONTRACT_SRC = './governedContract.js'; +const GOVERNED_CONTRACT2_SRC = './governedContract2.js'; + +const setUpGovernedContract = async (zoe, timer, EV, controller) => { + const installBundle = contractBundle => EV(zoe).install(contractBundle); + const installBundleToVatAdmin = contractBundle => + controller.validateAndInstallBundle(contractBundle); + const source = `${dirname}/${GOVERNED_CONTRACT_SRC}`; + const source2 = `${dirname}/${GOVERNED_CONTRACT2_SRC}`; + const governedContractBundle = await bundleSource(source); + const governedContract2Bundle = await bundleSource(source2); + + const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames'); + const governorInstallation = await EV(agoricNames).lookup( + 'installation', + 'contractGovernor', + ); + const voteCounterInstallation = await EV(agoricNames).lookup( + 'installation', + 'binaryVoteCounter', + ); + + const electorateCreatorFacet = await EV.vat('bootstrap').consumeItem( + 'economicCommitteeCreatorFacet', + ); + const poserInvitation = await EV(electorateCreatorFacet).getPoserInvitation(); + const poserInvitation2 = await EV( + electorateCreatorFacet, + ).getPoserInvitation(); + + const invitationIssuer = await EV(zoe).getInvitationIssuer(); + const invitationAmount = + await EV(invitationIssuer).getAmountOf(poserInvitation); + + const governedTerms = { + governedParams: { + [MALLEABLE_NUMBER]: { + type: ParamTypes.NAT, + value: 602214090000000000000000n, + }, + [CONTRACT_ELECTORATE]: { + type: ParamTypes.INVITATION, + value: invitationAmount, + }, + }, + governedApis: ['governanceApi'], + }; + + const governedInstallation = await installBundle(governedContractBundle); + await installBundleToVatAdmin(governedContract2Bundle); + const governorTerms = { + timer, + governedContractInstallation: governedInstallation, + governed: { + terms: governedTerms, + issuerKeywordRecord: {}, + }, + }; + + const governorFacets = await EV(zoe).startInstance( + governorInstallation, + {}, + governorTerms, + { + governed: { + initialPoserInvitation: poserInvitation, + }, + }, + ); + + return { + governorFacets, + invitationAmount, + voteCounterInstallation, + contract2SHA: governedContract2Bundle.endoZipBase64Sha512, + poserInvitation2, + }; +}; + +// A more minimal set would be better. We need governance, but not econ vats. +const PLATFORM_CONFIG = '@agoric/vm-config/decentral-test-vaults-config.json'; + +const makeDefaultTestContext = async t => { + console.time('DefaultTestContext'); + const swingsetTestKit = await makeSwingsetTestKit(t.log, undefined, { + configSpecifier: PLATFORM_CONFIG, + }); + + const { runUtils, storage, controller } = swingsetTestKit; + console.timeLog('DefaultTestContext', 'swingsetTestKit'); + const { EV } = runUtils; + const zoe: ZoeService = await EV.vat('bootstrap').consumeItem('zoe'); + const timer = await EV.vat('bootstrap').consumeItem('chainTimerService'); + + const facets = await setUpGovernedContract(zoe, timer, EV, controller); + + return { ...swingsetTestKit, facets }; +}; + +const test = anyTest as TestFn< + Awaited> +>; + +test.before(async t => { + t.context = await makeDefaultTestContext(t); +}); + +test.after.always(t => { + return t.context.shutdown && t.context.shutdown(); +}); + +test(`start contract; verify`, async t => { + const { runUtils, facets } = t.context; + const { + governorFacets: { creatorFacet }, + } = facets; + const { EV } = runUtils; + const contractPublicFacet = await EV(creatorFacet).getPublicFacet(); + + const avogadro = await EV(contractPublicFacet).getNum(); + t.is(await EV(contractPublicFacet).getApiCalled(), 0); + t.is(avogadro, 602214090000000000000000n); +}); + +test(`verify API governance`, async t => { + const { runUtils, facets } = t.context; + const { + governorFacets: { creatorFacet }, + voteCounterInstallation: vci, + } = facets; + + const { EV } = runUtils; + + const question = await EV(creatorFacet).voteOnApiInvocation( + 'add1', + [], + vci, + 37n, + ); + t.truthy(question.instance); + + await t.throwsAsync( + () => EV(creatorFacet).voteOnApiInvocation('add2', [], vci, 37n), + { + message: /"add2" is not a governed API./, + }, + ); +}); + +test(`upgrade; verify enhanced API governance`, async t => { + const { runUtils, facets } = t.context; + const { + governorFacets: { creatorFacet }, + voteCounterInstallation: vci, + contract2SHA, + poserInvitation2, + } = facets; + + const { EV } = runUtils; + const af = await EV(creatorFacet).getAdminFacet(); + + await EV(af).upgradeContract(`b1-${contract2SHA}`, { + initialPoserInvitation: poserInvitation2, + }); + + const question2 = await EV(creatorFacet).voteOnApiInvocation( + 'add2', + [], + vci, + 37n, + ); + t.truthy(question2.instance); +});