diff --git a/a3p-integration/proposals/z:acceptance/.gitignore b/a3p-integration/proposals/z:acceptance/.gitignore index 7f5da265d56..dc7df2c53f4 100644 --- a/a3p-integration/proposals/z:acceptance/.gitignore +++ b/a3p-integration/proposals/z:acceptance/.gitignore @@ -3,3 +3,5 @@ restart-valueVow start-valueVow localchaintest-submission recorded-instances-submission +upgrade-vaultFactory +upgrade-provisionPool diff --git a/a3p-integration/proposals/z:acceptance/governance.test.js b/a3p-integration/proposals/z:acceptance/governance.test.js index eaad279754d..e63a3271030 100644 --- a/a3p-integration/proposals/z:acceptance/governance.test.js +++ b/a3p-integration/proposals/z:acceptance/governance.test.js @@ -1,57 +1,35 @@ -/* global fetch setTimeout */ +/* global fetch */ import test from 'ava'; -import { makeWalletUtils } from '@agoric/client-utils'; import { GOV1ADDR, GOV2ADDR } from '@agoric/synthetic-chain'; import { makeGovernanceDriver } from './test-lib/governance.js'; -import { networkConfig } from './test-lib/index.js'; -import { makeTimerUtils } from './test-lib/utils.js'; +import { agdWalletUtils } from './test-lib/index.js'; +import { upgradeContract } from './test-lib/utils.js'; +import { networkConfig } from './test-lib/rpc.js'; const GOV4ADDR = 'agoric1c9gyu460lu70rtcdp95vummd6032psmpdx7wdy'; const governanceAddresses = [GOV4ADDR, GOV2ADDR, GOV1ADDR]; -// TODO test-lib export `walletUtils` with this ambient authority like s:stake-bld has -/** @param {number} ms */ -const delay = ms => - new Promise(resolve => setTimeout(() => resolve(undefined), ms)); +const { getLastUpdate, readLatestHead } = agdWalletUtils; +const governanceDriver = await makeGovernanceDriver(fetch, networkConfig); -const testSkipXXX = test.skip; // same lenth as test.serial to avoid reformatting all lines - -// z:acceptance governance fails/flakes: No quorum #10708 -testSkipXXX( +test.serial( 'economic committee can make governance proposal and vote on it', async t => { - const { waitUntil } = makeTimerUtils({ setTimeout }); - const { readLatestHead, getLastUpdate, getCurrentWalletRecord } = - await makeWalletUtils({ delay, fetch }, networkConfig); - const governanceDriver = await makeGovernanceDriver(fetch, networkConfig); - - /** @type {any} */ - const instance = await readLatestHead(`published.agoricNames.instance`); - const instances = Object.fromEntries(instance); - - const wallet = await getCurrentWalletRecord(governanceAddresses[0]); - const usedInvitations = wallet.offerToUsedInvitation; - - const charterInvitation = usedInvitations.find( - v => - v[1].value[0].instance.getBoardId() === - instances.econCommitteeCharter.getBoardId(), - ); - assert(charterInvitation, 'missing charter invitation'); - const params = { ChargingPeriod: 400n, }; const path = { paramPath: { key: 'governedParams' } }; t.log('Proposing param change', { params, path }); + const instanceName = 'VaultFactory'; - await governanceDriver.proposeVaultDirectorParamChange( + await governanceDriver.proposeParamChange( governanceAddresses[0], params, path, - charterInvitation[0], + instanceName, + 30, ); const questionUpdate = await getLastUpdate(governanceAddresses[0]); @@ -62,22 +40,9 @@ testSkipXXX( t.log('Voting on param change'); for (const address of governanceAddresses) { - const voteWallet = - /** @type {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} */ ( - await readLatestHead(`published.wallet.${address}.current`) - ); + const committeeInvitationForVoter = + await governanceDriver.getCommitteeInvitation(address); - const usedInvitationsForVoter = voteWallet.offerToUsedInvitation; - - const committeeInvitationForVoter = usedInvitationsForVoter.find( - v => - v[1].value[0].instance.getBoardId() === - instances.economicCommittee.getBoardId(), - ); - assert( - committeeInvitationForVoter, - `${address} must have committee invitation`, - ); await governanceDriver.voteOnProposedChanges( address, committeeInvitationForVoter[0], @@ -90,22 +55,168 @@ testSkipXXX( }); } - const latestQuestion = - /** @type {import('@agoric/governance/src/types.js').QuestionSpec} */ ( - await readLatestHead( - 'published.committees.Economic_Committee.latestQuestion', - ) + await governanceDriver.waitForElection(); + }, +); + +test.serial( + 'VaultFactory governed parameters are intact following contract upgrade', + async t => { + /** @type {any} */ + const vaultFactoryParamsBefore = await readLatestHead( + 'published.vaultFactory.governance', + ); + + /* + * At the previous test ('economic committee can make governance proposal and vote on it') + * The value of ChargingPeriod was updated to 400 + * The 'published.vaultFactory.governance' node should reflect that change. + */ + t.is( + vaultFactoryParamsBefore.current.ChargingPeriod.value, + 400n, + 'vaultFactory ChargingPeriod parameter value is not the expected ', + ); + + await upgradeContract('upgrade-vaultFactory', 'zcf-b1-6c08a-vaultFactory'); + + const vaultFactoryParamsAfter = await readLatestHead( + 'published.vaultFactory.governance', + ); + + t.deepEqual( + vaultFactoryParamsAfter, + vaultFactoryParamsBefore, + 'vaultFactory governed parameters did not match', + ); + }, +); + +test.serial( + 'economic committee can make governance proposal for ProvisionPool', + async t => { + /** @type {any} */ + const brand = await readLatestHead(`published.agoricNames.brand`); + const brands = Object.fromEntries(brand); + + const params = { + PerAccountInitialAmount: { brand: brands.IST, value: 100_000n }, + }; + const path = { paramPath: { key: 'governedParams' } }; + const instanceName = 'provisionPool'; + + await governanceDriver.proposeParamChange( + governanceAddresses[0], + params, + path, + instanceName, + 30, + ); + + const questionUpdate = await getLastUpdate(governanceAddresses[0]); + t.like(questionUpdate, { + status: { numWantsSatisfied: 1 }, + }); + + for (const address of governanceAddresses) { + const committeeInvitationForVoter = + await governanceDriver.getCommitteeInvitation(address); + + await governanceDriver.voteOnProposedChanges( + address, + committeeInvitationForVoter[0], ); - t.log('Waiting for deadline', latestQuestion); - /** @type {bigint} */ - // @ts-expect-error assume POSIX seconds since epoch - const deadline = latestQuestion.closingRule.deadline; - await waitUntil(deadline); - - const latestOutcome = await readLatestHead( - 'published.committees.Economic_Committee.latestOutcome', + + const voteUpdate = await getLastUpdate(address); + t.like(voteUpdate, { + status: { numWantsSatisfied: 1 }, + }); + } + + await governanceDriver.waitForElection(); + }, +); + +test.serial( + 'ProvisionPool governed parameters are intact following contract upgrade', + async t => { + /** @type {any} */ + const provisionPoolParamsBefore = await readLatestHead( + 'published.provisionPool.governance', + ); + + /* + * At the previous test ('economic committee can make governance proposal and vote on it') + * The value of ChargingPeriod was updated to 400 + * The 'published.vaultFactory.governance' node should reflect that change. + */ + t.is( + provisionPoolParamsBefore.current.PerAccountInitialAmount.value.value, + 100_000n, + 'provisionPool PerAccountInitialAmount parameter value is not the expected ', + ); + + await upgradeContract( + 'upgrade-provisionPool', + 'zcf-b1-db93f-provisionPool', + ); + + /** @type {any} */ + const provisionPoolParamsAfter = await readLatestHead( + 'published.provisionPool.governance', + ); + + t.deepEqual( + provisionPoolParamsAfter.current.PerAccountInitialAmount, + provisionPoolParamsBefore.current.PerAccountInitialAmount, + 'provisionPool governed parameters did not match', ); - t.log('Verifying latest outcome', latestOutcome); - t.like(latestOutcome, { outcome: 'win' }); }, ); + +test.serial('Governance proposals history is visible', async t => { + /* + * List ordered from most recent to earliest of Economic Committee + * parameter changes proposed prior to the execution of this test. + * + * XXX a dynamic solution should replace this hardcoded list to ensure + * the acceptance tests scalability + */ + const expectedParametersChanges = [ + ['PerAccountInitialAmount'], // z:acceptance/governance.test.js + ['ChargingPeriod'], // z:acceptance/governance.test.js + ['DebtLimit'], // z:acceptance/vaults.test.js + ['GiveMintedFee', 'MintLimit', 'WantMintedFee'], // z:acceptance/psm.test.js + ['DebtLimit'], // z:acceptance/scripts/test-vaults.mts + ['ClockStep', 'PriceLockPeriod', 'StartFrequency'], // z:acceptance/scripts/test-vaults.mts + ['DebtLimit'], // agoric-3-proposals/proposals/34:upgrade-10/performActions.js + ['ClockStep', 'PriceLockPeriod', 'StartFrequency'], // agoric-3-proposals/proposals/34:upgrade-10/performActions.js + ]; + + // history of Economic Committee parameters changes proposed since block height 0 + const history = await governanceDriver.getLatestQuestionHistory(); + t.true( + history.length > 0, + 'published.committees.Economic_Committee.latestQuestion node should not be empty', + ); + + const changedParameters = history.map(changes => Object.keys(changes)); + + /* + * In case you see the error message bellow and you + * executed an VoteOnParamChange offer prior to this test, + * please make sure to update the expectedParametersChanges list. + */ + if ( + !( + JSON.stringify(changedParameters) === + JSON.stringify(expectedParametersChanges) + ) + ) { + console.error( + `ERROR: Economic_Committee parameters changes history does not match with the expected list`, + ); + t.log('Economic_Committee parameters changes history: ', changedParameters); + t.log('Expected parameters changes history: ', expectedParametersChanges); + } +}); diff --git a/a3p-integration/proposals/z:acceptance/package.json b/a3p-integration/proposals/z:acceptance/package.json index fd4b89562ae..9afa351d90a 100644 --- a/a3p-integration/proposals/z:acceptance/package.json +++ b/a3p-integration/proposals/z:acceptance/package.json @@ -5,7 +5,9 @@ "testing/start-valueVow.js start-valueVow", "testing/recorded-retired-instances.js recorded-instances-submission", "vats/test-localchain.js localchaintest-submission", - "testing/restart-valueVow.js restart-valueVow" + "testing/restart-valueVow.js restart-valueVow", + "testing/upgrade-vaultFactory.js upgrade-vaultFactory", + "vats/upgrade-provisionPool.js upgrade-provisionPool" ] }, "type": "module", diff --git a/a3p-integration/proposals/z:acceptance/test-lib/governance.js b/a3p-integration/proposals/z:acceptance/test-lib/governance.js index 27c881c0ed2..a93d14411bd 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/governance.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/governance.js @@ -1,16 +1,29 @@ +/* global setTimeout */ + import { agops, agoric, executeOffer } from '@agoric/synthetic-chain'; -import { makeVstorageKit } from '@agoric/client-utils'; +import { + retryUntilCondition, + waitUntilElectionResult, +} from '@agoric/client-utils'; +import { agdWalletUtils } from './index.js'; +import { + checkCommitteeElectionResult, + fetchLatestEcQuestion, +} from './psm-lib.js'; +import { makeVstorageKit } from './rpc.js'; /** * @param {typeof window.fetch} fetch - * @param {import('@agoric/client-utils').MinimalNetworkConfig} networkConfig + * @param {import('./rpc.js').MinimalNetworkConfig} networkConfig */ export const makeGovernanceDriver = async (fetch, networkConfig) => { - const { readLatestHead, marshaller } = await makeVstorageKit( + const { readLatestHead, marshaller, vstorage } = await makeVstorageKit( { fetch }, networkConfig, ); + let deadline; + /** @param {string} previousOfferId */ const generateVoteOffer = async previousOfferId => { const latestQuestionRecord = @@ -63,7 +76,7 @@ export const makeGovernanceDriver = async (fetch, networkConfig) => { }; /** - * Generates a vault director parameter change proposal as a `executeOffer` message + * Generates a parameter change proposal as a `executeOffer` message * body. * * @param {string} previousOfferId - the `id` of the offer that this proposal is @@ -72,13 +85,15 @@ export const makeGovernanceDriver = async (fetch, networkConfig) => { * be open for (in seconds) * @param {any} params * @param {{ paramPath: any; }} paramsPath + * @param {string} instanceName * @returns {Promise} - the `executeOffer` message body as a JSON string */ - const generateVaultDirectorParamChange = async ( + const generateParamChange = async ( previousOfferId, voteDur, params, paramsPath, + instanceName, ) => { const instancesRaw = await agoric.follow( '-lF', @@ -89,12 +104,12 @@ export const makeGovernanceDriver = async (fetch, networkConfig) => { const instances = Object.fromEntries( marshaller.fromCapData(JSON.parse(instancesRaw)), ); - const { VaultFactory } = instances; - assert(VaultFactory); + const instance = instances[instanceName]; + assert(instance); const msSinceEpoch = Date.now(); const id = `propose-${msSinceEpoch}`; - const deadline = BigInt(Math.ceil(msSinceEpoch / 1000)) + BigInt(voteDur); + deadline = BigInt(Math.ceil(msSinceEpoch / 1000)) + BigInt(voteDur); const body = { method: 'executeOffer', offer: { @@ -106,7 +121,7 @@ export const makeGovernanceDriver = async (fetch, networkConfig) => { }, offerArgs: { deadline, - instance: VaultFactory, + instance, params, path: paramsPath, }, @@ -123,12 +138,16 @@ export const makeGovernanceDriver = async (fetch, networkConfig) => { * @param {string} address * @param {any} params * @param {{paramPath: any}} path - * @param {string} charterAcceptOfferId + * @param {string} instanceName + * @param {number} votingDuration + * @param {string} [charterAcceptOfferId] */ - const proposeVaultDirectorParamChange = async ( + const proposeParamChange = async ( address, params, path, + instanceName, + votingDuration, charterAcceptOfferId, ) => { await null; @@ -144,12 +163,107 @@ export const makeGovernanceDriver = async (fetch, networkConfig) => { return executeOffer( address, - generateVaultDirectorParamChange(offerId, 10, params, path), + generateParamChange(offerId, votingDuration, params, path, instanceName), + ); + }; + + const getCharterInvitation = async address => { + const { getCurrentWalletRecord } = agdWalletUtils; + + /** @type {any} */ + const instance = await readLatestHead(`published.agoricNames.instance`); + const instances = Object.fromEntries(instance); + + const wallet = await getCurrentWalletRecord(address); + const usedInvitations = wallet.offerToUsedInvitation; + + const charterInvitation = usedInvitations.find( + v => + v[1].value[0].instance.getBoardId() === + instances.econCommitteeCharter.getBoardId(), + ); + assert(charterInvitation, 'missing charter invitation'); + + return charterInvitation; + }; + + const getCommitteeInvitation = async address => { + /** @type {any} */ + const instance = await readLatestHead(`published.agoricNames.instance`); + const instances = Object.fromEntries(instance); + + const voteWallet = + /** @type {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} */ ( + await readLatestHead(`published.wallet.${address}.current`) + ); + + const usedInvitationsForVoter = voteWallet.offerToUsedInvitation; + + const committeeInvitationForVoter = usedInvitationsForVoter.find( + v => + v[1].value[0].instance.getBoardId() === + instances.economicCommittee.getBoardId(), ); + assert( + committeeInvitationForVoter, + `${address} must have committee invitation`, + ); + + return committeeInvitationForVoter; + }; + + const getLatestQuestion = async () => { + const { latestOutcome, latestQuestion } = await retryUntilCondition( + () => fetchLatestEcQuestion({ follow: agoric.follow }), + electionResult => + checkCommitteeElectionResult(electionResult, { + outcome: 'win', + deadline, + }), + 'Governed param change election failed', + { setTimeout, retryIntervalMs: 5000, maxRetries: 15 }, + ); + + return { latestOutcome, latestQuestion }; + }; + + const waitForElection = () => + waitUntilElectionResult( + 'published.committees.Economic_Committee', + { outcome: 'win', deadline }, + // @ts-expect-error vstorage casting + { vstorage: { readLatestHead }, log: console.log, setTimeout }, + { + errorMessage: 'Governed param change election failed', + retryIntervalMs: 5000, + maxRetries: 15, + }, + ); + + const getLatestQuestionHistory = async () => { + const nodePath = 'published.committees.Economic_Committee.latestQuestion'; + + const historyIterator = await vstorage.readHistory(nodePath); + const history = []; + + for await (const data of historyIterator) { + if (data) { + const question = marshaller.fromCapData(JSON.parse(data[0])); + const changes = question.positions[0].changes; + history.push(changes); + } + } + + return history; }; return { voteOnProposedChanges, - proposeVaultDirectorParamChange, + proposeParamChange, + getCharterInvitation, + getCommitteeInvitation, + getLatestQuestion, + waitForElection, + getLatestQuestionHistory, }; }; diff --git a/a3p-integration/proposals/z:acceptance/test-lib/psm-lib.js b/a3p-integration/proposals/z:acceptance/test-lib/psm-lib.js index 74763e6f401..24c6a89ff88 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/psm-lib.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/psm-lib.js @@ -218,7 +218,7 @@ export const fetchLatestEcQuestion = async io => { return { latestOutcome, latestQuestion }; }; -const checkCommitteeElectionResult = ( +export const checkCommitteeElectionResult = ( /** @type {{ latestOutcome: { outcome: any; question: any; }; latestQuestion: { closingRule: { deadline: any; }; questionHandle: any; }; }} */ electionResult, /** @type {{ outcome: any; deadline: any; }} */ expectedResult, ) => { diff --git a/a3p-integration/proposals/z:acceptance/test-lib/rpc.js b/a3p-integration/proposals/z:acceptance/test-lib/rpc.js new file mode 100644 index 00000000000..1a401875f86 --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/test-lib/rpc.js @@ -0,0 +1,300 @@ +/** + * @file This file implements methods currently available in + * packages/client-utils . + * + * With the exceptions of: + * - makeVstorage and mapHistory: copied from `multichain-testing/tools/batchQuery.js`. + * - makeAPI: copied from `multichain-testing/tools/makeHttpClient.js`. + * + * These modifications were made to address the issue described in #10574. + */ + +// @ts-check + +import { + boardSlottingMarshaller, + makeBoardRemote, +} from '@agoric/internal/src/marshal.js'; +import { E, Far } from '@endo/far'; +import { Fail } from '@endo/errors'; + +export { boardSlottingMarshaller }; + +/** @type {(val: any) => string} */ +export const boardValToSlot = val => { + if ('getBoardId' in val) { + return val.getBoardId(); + } + throw Fail`unknown obj in boardSlottingMarshaller.valToSlot ${val}`; +}; + +/** @param {string} agoricNetSubdomain */ +export const networkConfigUrl = agoricNetSubdomain => + `https://${agoricNetSubdomain}.agoric.net/network-config`; +/** @param {string} agoricNetSubdomain */ +export const rpcUrl = agoricNetSubdomain => + `https://${agoricNetSubdomain}.rpc.agoric.net:443`; + +/** + * @typedef {{ rpcAddrs: string[], chainName: string, apiAddress: string }} MinimalNetworkConfig + */ + +/** @type {MinimalNetworkConfig} */ +const networkConfig = { + rpcAddrs: ['http://0.0.0.0:26657'], + chainName: 'agoriclocal', + apiAddress: 'http://localhost:1317', +}; +export { networkConfig }; +// console.warn('networkConfig', networkConfig); + +/** + * gRPC-gateway REST API access + * + * @see {@link https://docs.cosmos.network/v0.45/core/grpc_rest.html#rest-server Cosmos SDK REST Server} + * + * Note: avoid Legacy REST routes, per + * {@link https://docs.cosmos.network/v0.45/migrations/rest.html Cosmos SDK REST Endpoints Migration}. + * + * @param {string} apiAddress nodes default to port 1317 + * @param {object} io + * @param {typeof fetch} io.fetch + */ +const makeAPI = (apiAddress, { fetch }) => { + assert.typeof(apiAddress, 'string'); + + /** + * @param {string} href + * @param {object} [options] + * @param {Record} [options.headers] + */ + const getJSON = (href, options = {}) => { + const opts = { + keepalive: true, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + }; + const url = `${apiAddress}${href}`; + return fetch(url, opts).then(r => { + if (!r.ok) throw Error(r.statusText); + return r.json().then(data => { + return data; + }); + }); + }; + + return Far('LCD', { + getJSON, + latestBlock: () => getJSON(`/cosmos/base/tendermint/v1beta1/blocks/latest`), + }); +}; +/** @typedef {ReturnType} LCD */ + +/** + * @template T + * @param {(value: string) => T} f + * @param {AsyncGenerator} chunks + */ +async function* mapHistory(f, chunks) { + for await (const chunk of chunks) { + if (chunk === undefined) continue; + for (const value of chunk.reverse()) { + yield f(value); + } + } +} + +/** + * @param {LCD} lcd + */ +export const makeVStorage = lcd => { + const getJSON = (href, options) => E(lcd).getJSON(href, options); + + // height=0 is the same as omitting height and implies the highest block + const href = (path = 'published', { kind = 'data' } = {}) => + `/agoric/vstorage/${kind}/${path}`; + const headers = height => + height ? { 'x-cosmos-block-height': `${height}` } : undefined; + + const readStorage = ( + path = 'published', + { kind = 'data', height = 0 } = {}, + ) => + getJSON(href(path, { kind }), { headers: headers(height) }).catch(err => { + throw Error(`cannot read ${kind} of ${path}: ${err.message}`); + }); + const readCell = (path, opts) => + readStorage(path, opts) + .then(data => data.value) + .then(s => (s === '' ? {} : JSON.parse(s))); + + /** + * Read values going back as far as available + * + * @param {string} path + * @param {number | string} [minHeight] + */ + async function* readHistory(path, minHeight = undefined) { + // undefined the first iteration, to query at the highest + let blockHeight; + await null; + do { + // console.debug('READING', { blockHeight }); + /** @type {string[]} */ + let values = []; + try { + ({ blockHeight, values } = await readCell(path, { + kind: 'data', + height: blockHeight && Number(blockHeight) - 1, + })); + // console.debug('readAt returned', { blockHeight }); + } catch (err) { + if (err.message.match(/unknown request/)) { + // XXX FIXME + // console.error(err); + break; + } + throw err; + } + yield values; + // console.debug('PUSHED', values); + // console.debug('NEW', { blockHeight, minHeight }); + if (minHeight && Number(blockHeight) <= Number(minHeight)) break; + } while (blockHeight > 0); + } + + /** + * @template T + * @param {(value: string) => T} f + * @param {string} path + * @param {number | string} [minHeight] + */ + const readHistoryBy = (f, path, minHeight) => + mapHistory(f, readHistory(path, minHeight)); + + return { + lcd, + readLatest: readStorage, + readCell, + readHistory, + readHistoryBy, + }; +}; +/** @typedef {ReturnType} VStorage */ + +export const makeFromBoard = () => { + const cache = new Map(); + /** @type {(boardId: string, iface?: string) => ReturnType} */ + const convertSlotToVal = (boardId, iface) => { + if (cache.has(boardId)) { + return cache.get(boardId); + } + const val = makeBoardRemote({ boardId, iface }); + cache.set(boardId, val); + return val; + }; + return harden({ convertSlotToVal }); +}; +/** @typedef {ReturnType} IdMap */ + +export const storageHelper = { + parseCapData: txt => { + /** @type {{ value: string }} */ + const { value } = txt; + assert(typeof value === 'string', typeof value); + const specimen = JSON.parse(value); + const { blockHeight, values } = specimen; + assert(values, `empty values in specimen ${value}`); + const capDatas = storageHelper.parseMany(values); + return { blockHeight, capDatas }; + }, + /** + * @param {string} txt + * @param {IdMap} ctx + */ + unserializeTxt: (txt, ctx) => { + const { capDatas } = storageHelper.parseCapData(txt); + return capDatas.map(capData => + boardSlottingMarshaller(ctx.convertSlotToVal).fromCapData(capData), + ); + }, + /** @param {string[]} capDataStrings array of stringified capData */ + parseMany: capDataStrings => { + assert(capDataStrings && capDataStrings.length); + /** @type {{ body: string, slots: string[] }[]} */ + const capDatas = capDataStrings.map(s => JSON.parse(s)); + for (const capData of capDatas) { + assert(typeof capData === 'object' && capData !== null); + assert('body' in capData && 'slots' in capData); + assert(typeof capData.body === 'string'); + assert(Array.isArray(capData.slots)); + } + return capDatas; + }, +}; +harden(storageHelper); + +/** + * @param {IdMap} ctx + * @param {VStorage} vstorage + * @returns {Promise} + */ +export const makeAgoricNames = async (ctx, vstorage) => { + /** @type {Record} */ + const reverse = {}; + const entries = await Promise.all( + ['brand', 'instance', 'vbankAsset'].map(async kind => { + const content = await vstorage.readLatest( + `published.agoricNames.${kind}`, + ); + /** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */ + const parts = storageHelper.unserializeTxt(content, ctx).at(-1); + for (const [name, remote] of parts) { + if ('getBoardId' in remote) { + reverse[/** @type {string} */ (remote.getBoardId())] = name; + } + } + return [kind, Object.fromEntries(parts)]; + }), + ); + return { ...Object.fromEntries(entries), reverse }; +}; + +/** + * @param {{ fetch: typeof window.fetch }} io + * @param {MinimalNetworkConfig} config + */ +export const makeVstorageKit = async ({ fetch }, config = networkConfig) => { + await null; + try { + const lcd = await makeAPI(networkConfig.apiAddress, { fetch }); + const vstorage = makeVStorage(lcd); + + const fromBoard = makeFromBoard(); + const agoricNames = await makeAgoricNames(fromBoard, vstorage); + + const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal); + + /** @type {(txt: string) => unknown} */ + const unserializeHead = txt => + storageHelper.unserializeTxt(txt, fromBoard).at(-1); + + /** @type {(path: string) => Promise} */ + const readLatestHead = path => + vstorage.readLatest(path).then(unserializeHead); + + return { + agoricNames, + fromBoard, + marshaller, + readLatestHead, + unserializeHead, + vstorage, + }; + } catch (err) { + throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`); + } +}; +/** @typedef {Awaited>} RpcUtils */ diff --git a/a3p-integration/proposals/z:acceptance/test-lib/utils.js b/a3p-integration/proposals/z:acceptance/test-lib/utils.js index 0826daa3660..603e7cb8d2c 100644 --- a/a3p-integration/proposals/z:acceptance/test-lib/utils.js +++ b/a3p-integration/proposals/z:acceptance/test-lib/utils.js @@ -3,7 +3,9 @@ import { LOCAL_CONFIG, makeStargateClient, makeVstorageKit, + retryUntilCondition, } from '@agoric/client-utils'; +import { evalBundles, getDetailsMatchingVats } from '@agoric/synthetic-chain'; import { readFile, writeFile } from 'node:fs/promises'; export const stargateClientP = makeStargateClient(LOCAL_CONFIG, { fetch }); @@ -93,3 +95,29 @@ export const makeTimerUtils = ({ setTimeout }) => { waitUntil, }; }; + +/** + * This function solves the limitation of getIncarnation when multiple Vats + * are returned for the provided vatName and does not return the incarnation + * of the desired Vat (e.g. zcf-mintHolder-USDC) + * @param {string} vatName + * @returns {Promise} + */ +const getIncarnationFromDetails = async vatName => { + const matchingVats = await getDetailsMatchingVats(vatName); + const expectedVat = matchingVats.find(vat => vat.vatName === vatName); + assert(expectedVat, `No matching Vat was found for ${vatName}`); + return expectedVat.incarnation; +}; + +export const upgradeContract = async (submissionPath, vatName) => { + const incarnationBefore = await getIncarnationFromDetails(vatName); + await evalBundles(submissionPath); + + return retryUntilCondition( + async () => getIncarnationFromDetails(vatName), + value => value === incarnationBefore + 1, + `${vatName} upgrade not processed yet`, + { setTimeout, retryIntervalMs: 5000, maxRetries: 15 }, + ); +}; diff --git a/a3p-integration/proposals/z:acceptance/test.sh b/a3p-integration/proposals/z:acceptance/test.sh index e45cced45d1..78e9c2d09fa 100755 --- a/a3p-integration/proposals/z:acceptance/test.sh +++ b/a3p-integration/proposals/z:acceptance/test.sh @@ -21,18 +21,18 @@ yarn ava kread.test.js echo ACCEPTANCE TESTING valueVow yarn ava valueVow.test.js -echo ACCEPTANCE TESTING state sync -./state-sync-snapshots-test.sh -./genesis-test.sh - echo ACCEPTANCE TESTING wallet yarn ava wallet.test.js echo ACCEPTANCE TESTING psm yarn ava psm.test.js +echo ACCEPTANCE TESTING vaults +yarn ava vaults.test.js + echo ACCEPTANCE TESTING governance yarn ava governance.test.js -echo ACCEPTANCE TESTING vaults -yarn ava vaults.test.js +echo ACCEPTANCE TESTING state sync +./state-sync-snapshots-test.sh +./genesis-test.sh diff --git a/packages/builders/scripts/testing/upgrade-vaultFactory.js b/packages/builders/scripts/testing/upgrade-vaultFactory.js new file mode 100644 index 00000000000..fa12d22ddcc --- /dev/null +++ b/packages/builders/scripts/testing/upgrade-vaultFactory.js @@ -0,0 +1,21 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/vats/src/proposals/upgrade-vaultFactory-proposal.js', + getManifestCall: [ + 'getManifestForVaultFactoryUpgrade', + { + contractRef: publishRef( + install('@agoric/inter-protocol/src/vaultFactory/vaultFactory.js'), + ), + }, + ], + }); + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */ +export default async (homeP, endowments) => { + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('upgrade-vaults', defaultProposalBuilder); +}; diff --git a/packages/inter-protocol/src/proposals/upgrade-vaults.js b/packages/inter-protocol/src/proposals/upgrade-vaults.js index febaa8cfa35..b92961d871f 100644 --- a/packages/inter-protocol/src/proposals/upgrade-vaults.js +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -1,3 +1,9 @@ +/** + * @file this core-eval proposal is specific to the upgrade-18 scenario, + * handling tasks beyond generic Vault Factory null upgrade. For a reusable + * proposal, see upgrade-vaultFactory-proposal.js. + */ + import { E } from '@endo/far'; import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; import { makeTracer } from '@agoric/internal/src/index.js'; diff --git a/packages/vats/src/proposals/upgrade-vaultFactory-proposal.js b/packages/vats/src/proposals/upgrade-vaultFactory-proposal.js new file mode 100644 index 00000000000..242bdd6ae93 --- /dev/null +++ b/packages/vats/src/proposals/upgrade-vaultFactory-proposal.js @@ -0,0 +1,124 @@ +/** + * @file this core-eval proposal is a generic and reusable script for executing + * a Vault Factory upgrade. In contrast, upgrade-vaults.js is a specific + * implementation tailored to the upgrade-18. + */ + +import { E } from '@endo/far'; +import { makeTracer } from '@agoric/internal/src/index.js'; +import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; + +const trace = makeTracer('upgrade Vaults proposal'); + +export const upgradeVaultFactory = async (powers, options) => { + trace('Initiate VaultFactory contract upgrade'); + + const { + consume: { + zoe, + vaultFactoryKit, + reserveKit, + auctioneerKit, + economicCommitteeCreatorFacet, + }, + } = powers; + + const { + options: { contractRef }, + } = options; + + const { adminFacet, privateArgs, publicFacet, instance } = + await vaultFactoryKit; + + const allBrands = await E(zoe).getBrands(instance); + const { Minted: _istBrand, ...vaultBrands } = allBrands; + + const initialPoserInvitation = await E( + economicCommitteeCreatorFacet, + ).getPoserInvitation(); + + const initialShortfallInvitation = await E( + E.get(reserveKit).creatorFacet, + ).makeShortfallReportingInvitation(); + + const auctioneerInstance = await E.get(auctioneerKit).instance; + + const readCurrentDirectorParams = async () => { + await null; + + const subscription = E(publicFacet).getElectorateSubscription(); + const notifier = makeNotifierFromAsyncIterable(subscription); + const { updateCount } = await notifier.getUpdateSince(); + + // subscribeAfter() retrieves the latest value. + const after = await E(subscription).subscribeAfter(updateCount); + const { current } = after.head.value; + return harden({ + MinInitialDebt: current.MinInitialDebt.value, + ReferencedUI: current.ReferencedUI.value, + RecordingPeriod: current.RecordingPeriod.value, + ChargingPeriod: current.ChargingPeriod.value, + }); + }; + + const directorParamOverrides = await readCurrentDirectorParams(); + trace({ directorParamOverrides }); + + const readManagerParams = async () => { + await null; + + const params = {}; + for (const kwd of Object.keys(vaultBrands)) { + const collateralBrand = vaultBrands[kwd]; + + /** @type {any} */ + const governedParams = await E(publicFacet).getGovernedParams({ + collateralBrand, + }); + params[kwd] = harden({ + brand: collateralBrand, + debtLimit: governedParams.DebtLimit.value, + interestRate: governedParams.InterestRate.value, + liquidationMargin: governedParams.LiquidationMargin.value, + liquidationPadding: governedParams.LiquidationPadding.value, + liquidationPenalty: governedParams.LiquidationPenalty.value, + mintFee: governedParams.MintFee.value, + }); + trace(kwd, params[kwd]); + } + return params; + }; + const managerParams = await readManagerParams(); + + const newPrivateArgs = harden({ + ...privateArgs, + auctioneerInstance, + initialPoserInvitation, + initialShortfallInvitation, + managerParams, + directorParamOverrides, + }); + + await E(adminFacet).upgradeContract(contractRef.bundleID, newPrivateArgs); + + trace('VaultFactory contract upgraded!'); +}; + +export const getManifestForVaultFactoryUpgrade = ( + { restoreRef }, + { contractRef }, +) => ({ + manifest: { + [upgradeVaultFactory.name]: { + consume: { + zoe: true, + vaultFactoryKit: true, + reserveKit: true, + auctioneerKit: true, + economicCommitteeCreatorFacet: true, + }, + }, + }, + installations: { vaultFactory: restoreRef(contractRef) }, + options: { contractRef }, +});