diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js index 31035b70a90..4dd831daed7 100644 --- a/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/agoric-upgrade-11/mn2-start.test.js @@ -30,6 +30,7 @@ import anyTest from 'ava'; import * as cpAmbient from 'child_process'; // TODO: use execa import * as fspAmbient from 'fs/promises'; +import { tmpName as tmpNameAmbient } from 'tmp'; import * as pathAmbient from 'path'; import * as processAmbient from 'process'; import dbOpenAmbient from 'better-sqlite3'; @@ -47,45 +48,51 @@ import { makeAgd } from '../lib/agd-lib.js'; import { Far, makeMarshal, makeTranslationTable } from '../lib/unmarshal.js'; import { Fail, NonNullish } from '../lib/assert.js'; import { dbTool } from '../lib/vat-status.js'; +import { makeFileRW, makeWebCache, makeWebRd } from '../lib/webAsset.js'; /** @typedef {Awaited>} TestContext */ /** @type {import('ava').TestFn}} */ -let test = anyTest; - -// If $MN2_PROPOSAL_INFO is not set, no tests are run. -if (!('MN2_PROPOSAL_INFO' in processAmbient.env)) { - console.log('MN2_PROPOSAL_INFO not set. Skipping all tests.'); - const noop = (..._args) => {}; - // @ts-expect-error - test = noop; - Object.assign(test, { before: noop, serial: noop }); -} +const test = anyTest; /** - * Reify file read access as an object. + * URLs of release assets, including bundle hashes agreed by BLD stakers * - * @param {string} root - * @param {object} io - * @param {Pick} io.fsp - * @param {Pick} io.path + * TODO: get permits, scripts from blockchain? + * TODO: verify bundle contents against hashes? * - * @typedef {ReturnType} FileRd + * KREAd-rc1 to Mainnet + * voting 2023-09-28 to 2023-10-01 + * https://agoric.explorers.guru/proposal/53 */ -const makeFileRd = (root, { fsp, path }) => { - /** @param {string} there */ - const make = there => { - const self = { - toString: () => there, - /** @param {string[]} segments */ - join: (...segments) => make(path.join(there, ...segments)), - stat: () => fsp.stat(there), - readText: () => fsp.readFile(there, 'utf8'), - readDir: () => - fsp.readdir(there).then(names => names.map(ref => self.join(ref))), - }; - return self; - }; - return make(root); +const releaseInfo = { + url: 'https://github.com/Kryha/KREAd/releases/tag/KREAd-rc1', + /** @type {Record} */ + buildAssets: { + 'kread-committee-info.json': { + evals: [ + { + permit: 'kread-invite-committee-permit.json', + script: 'kread-invite-committee.js', + }, + ], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-51085a4ad4ac3448ccf039c0b54b41bd11e9367dfbd641deda38e614a7f647d7f1c0d34e55ba354d0331b1bf54c999fca911e6a796c90c30869f7fb8887b3024.json', + '/Users/wietzes/.agoric/cache/b1-a724453e7bfcaae1843be4532e18c1236c3d6d33bf6c44011f2966e155bc7149b904573014e583fdcde2b9cf2913cb8b337fc9daf79c59a38a37c99030fcf7dc.json', + ], + }, + 'start-kread-info.json': { + evals: [{ permit: 'start-kread-permit.json', script: 'start-kread.js' }], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-853acd6ba3993f0f19d6c5b0a88c9a722c9b41da17cf7f98ff7705e131860c4737d7faa758ca2120773632dbaf949e4bcce2a2cbf2db224fa09cd165678f64ac.json', + '/Users/wietzes/.agoric/cache/b1-0c3363b8737677076e141a84b84c8499012f6ba79c0871fc906c8be1bb6d11312a7d14d5a3356828a1de6baa4bee818a37b7cb1ca2064f6eecbabc0a40d28136.json', + ], + }, + }, +}; + +const dappAPI = { + instance: 'kread', // agoricNames.instance key + vstorageNode: 'kread', }; const staticConfig = { @@ -94,7 +101,9 @@ const staticConfig = { proposer: 'validator', collateralPrice: 6, // conservatively low price. TODO: look up swingstorePath: '~/.agoric/data/agoric/swingstore.sqlite', - initialCoins: `20000000ubld`, + releaseAssets: releaseInfo.url.replace('/tag/', '/download/') + '/', + buildInfo: Object.values(releaseInfo.buildAssets), + initialCoins: `20000000ubld`, // enough to provision a smartWallet accounts: { krgov1: { address: 'agoric1890064p6j3xhzzdf8daknd6kpvhw766ds8flgw', @@ -117,6 +126,7 @@ const staticConfig = { 'magic enrich village office myth depth upper pair april dad visit memory resemble castle lab surface globe debate chair upper army pony moon tone', }, }, + ...dappAPI, }; /** @@ -129,23 +139,20 @@ const makeTestContext = async (io = {}) => { dbOpen = dbOpenAmbient, fsp = fspAmbient, path = pathAmbient, + tmpName = tmpNameAmbient, } = io; - const theEnv = name => { - const value = env[name]; - if (value === undefined) { - throw Error(`$${name} required`); - } - return value; - }; + const src = makeWebRd(staticConfig.releaseAssets, { fetch }); + const td = await new Promise((resolve, reject) => + tmpName({ prefix: 'assets' }, (err, x) => (err ? reject(err) : resolve(x))), + ); + const dest = makeFileRW(td, { fsp, path }); + td.teardown(() => assets.remove()); + const assets = makeWebCache(src, dest); const config = { - proposalDir: makeFileRd(theEnv('MN2_PROPOSAL_INFO'), { fsp, path }), - instance: theEnv('MN2_INSTANCE'), // agoricNames.instance key - vstorageNode: theEnv('MN2_INSTANCE'), - // TODO: feeAddress: theEnv('FEE_ADDRESS'), - chainId: env.CHAINID || 'agoriclocal', - royaltyThingy: theEnv('GOV3ADDR'), + assets, + chainId: 'agoriclocal', ...staticConfig, }; @@ -155,7 +162,7 @@ const makeTestContext = async (io = {}) => { keyringBackend: 'test', }); - const dbPath = staticConfig.swingstorePath.replace(/^~/, theEnv('HOME')); + const dbPath = staticConfig.swingstorePath.replace(/^~/, env.HOME); const swingstore = dbTool(dbOpen(dbPath, { readonly: true })); return { agd, agoric, swingstore, config }; @@ -273,23 +280,6 @@ const loadedBundleIds = swingstore => { return ids; }; -/** - * @param {FileRd} rd - assumed to contain ProposalInfo - * @returns {Promise} - */ -const proposalInfo = async rd => { - const all = await rd.readDir(); - const runInfos = all.filter(f => `${f}`.endsWith('-info.json')); - return Promise.all( - runInfos.map(async infoRd => { - const txt = await infoRd.readText(); - /** @type {ProposalInfo} */ - const p = JSON.parse(txt); - return p; - }), - ); -}; - /** * @param {string} cacheFn - e.g. /home/me.agoric/cache/b1-DEADBEEF.json */ @@ -303,7 +293,7 @@ const bundleDetail = cacheFn => { test.serial('bundles not yet installed', async t => { const { swingstore, config } = t.context; const loaded = loadedBundleIds(swingstore); - const info = await proposalInfo(config.proposalDir); + const info = staticConfig.buildInfo; for (const { bundles, evals } of info) { t.log(evals[0].script, evals.length, 'eval', bundles.length, 'bundles'); for (const bundle of bundles) { @@ -316,16 +306,15 @@ test.serial('bundles not yet installed', async t => { /** @param {number[]} xs */ const sum = xs => xs.reduce((a, b) => a + b, 0); -/** @param {FileRd} proposalDir */ -const readBundleSizes = async proposalDir => { - const info = await proposalInfo(proposalDir); - const bundleRds = info - .map(({ bundles }) => - bundles.map(b => proposalDir.join('bundles', bundleDetail(b).fileName)), - ) - .flat(); +/** @param {import('../lib/webAsset.js').WebCache} assets */ +const readBundleSizes = async assets => { + const info = staticConfig.buildInfo; const bundleSizes = await Promise.all( - bundleRds.map(rd => rd.stat().then(st => st.size)), + info + .map(({ bundles }) => + bundles.map(b => assets.size(bundleDetail(b).fileName)), + ) + .flat(), ); const totalSize = sum(bundleSizes); return { bundleSizes, totalSize }; @@ -366,13 +355,13 @@ const txAbbr = tx => { test.serial('ensure bundles installed', async t => { const { agd, swingstore, agoric, config, io } = t.context; - const { chainId, proposalDir } = config; + const { chainId, assets } = config; const loaded = loadedBundleIds(swingstore); const from = agd.lookup(config.installer); let todo = 0; let done = 0; - for (const { bundles } of await proposalInfo(proposalDir)) { + for (const { bundles } of staticConfig.buildInfo) { todo += bundles.length; for (const bundle of bundles) { const { id, fileName, endoZipBase64Sha512 } = bundleDetail(bundle); @@ -382,7 +371,7 @@ test.serial('ensure bundles installed', async t => { continue; } - const bundleRd = proposalDir.join('bundles', fileName); + const bundleRd = await assets.storedPath(fileName); const result = await agd.tx( ['swingset', 'install-bundle', `@${bundleRd}`, '--gas', 'auto'], { from, chainId, yes: true }, @@ -443,27 +432,26 @@ test.serial('core eval prereqs: provision royalty, gov, ...', async t => { test.serial('core eval proposal passes', async t => { const { agd, swingstore, config } = t.context; const from = agd.lookup(config.proposer); - const { chainId, deposit, proposalDir, instance } = config; + const { chainId, deposit, assets, instance } = config; const info = { title: instance, description: `start ${instance}` }; t.log('submit proposal', instance); - console.debug('submit proposal', instance); // double-check that bundles are loaded const loaded = loadedBundleIds(swingstore); - const proposals = await proposalInfo(proposalDir); - for (const { bundles } of proposals) { + const { buildInfo } = staticConfig; + for (const { bundles } of buildInfo) { for (const bundle of bundles) { const { id } = bundleDetail(bundle); testIncludes(t, id, loaded, 'loaded bundles'); } } - const evalNames = proposals + const evalNames = buildInfo .map(({ evals }) => evals) .flat() .map(e => [e.permit, e.script]) .flat(); - const evalPaths = evalNames.map(e => proposalDir.join(e).toString()); + const evalPaths = await Promise.all(evalNames.map(e => assets.storedPath(e))); t.log(evalPaths); console.debug('await tx', evalPaths); const result = await agd.tx( @@ -496,6 +484,7 @@ test.serial(`agoricNames.instance is populated`, async t => { // needs 2 brand names test.todo(`agoricNames.brand is populated`); +test.todo('boardAux is populated'); test.serial('vstorage published.CHILD is present', async t => { const { agd, config } = t.context; diff --git a/packages/deployment/upgrade-test/upgrade-test-scripts/lib/webAsset.js b/packages/deployment/upgrade-test/upgrade-test-scripts/lib/webAsset.js new file mode 100644 index 00000000000..81a29835041 --- /dev/null +++ b/packages/deployment/upgrade-test/upgrade-test-scripts/lib/webAsset.js @@ -0,0 +1,183 @@ +import { tmpName } from 'tmp'; + +/** + * + * @param {string} root + * @param {{ fetch: typeof fetch }} io + * + * @typedef {ReturnType} TextRd + */ +export const makeWebRd = (root, { fetch }) => { + /** @param {string} there */ + const make = there => { + const join = (...segments) => { + let out = there; + for (const segment of segments) { + out = `${new URL(segment, out)}`; + } + return out; + }; + const self = { + toString: () => there, + /** @param {string[]} segments */ + join: (...segments) => make(join(...segments)), + readText: () => fetch(there).then(res => res.text()), + }; + return self; + }; + return make(root); +}; + +/** + * Reify file read access as an object. + * + * @param {string} root + * @param {object} io + * @param {Pick} io.fsp + * @param {Pick} io.path + * + * @typedef {ReturnType} FileRd + */ +export const makeFileRd = (root, { fsp, path }) => { + /** @param {string} there */ + const make = there => { + const self = { + toString: () => there, + /** @param {string[]} segments */ + join: (...segments) => make(path.join(there, ...segments)), + stat: () => fsp.stat(there), + readText: () => fsp.readFile(there, 'utf8'), + }; + return self; + }; + return make(root); +}; + +/** + * Reify file read/write access as an object. + * + * @param {string} root + * @param {object} io + * @param {Pick} io.fsp + * @param {Pick} io.path + * + * @typedef {ReturnType} FileRW + */ +export const makeFileRW = (root, { fsp, path }) => { + /** @param {string} there */ + const make = there => { + const ro = makeFileRd(there, { fsp, path }); + const self = { + toString: () => there, + readOnly: () => ro, + /** @param {string[]} segments */ + join: (...segments) => make(path.join(there, ...segments)), + writeText: text => fsp.writeFile(there, text, 'utf8'), + unlink: () => fsp.unlink(there), + mkdir: () => fsp.mkdir(there, { recursive: true }), + rmdir: () => fsp.rmdir(there), + }; + return self; + }; + return make(root); +}; + +/** + * @param {TextRd} src + * @param {FileRW} dest + * + * @typedef {ReturnType} WebCache + */ +export const makeWebCache = (src, dest) => { + /** @type {Map>} */ + const saved = new Map(); + + /** @param {string} segment */ + const getFileP = segment => { + const target = src.join(segment); + const addr = `${target}`; + let cached = saved.get(addr); + if (cached) return cached; + + const f = dest.join(segment); + /** @type {Promise} */ + const p = new Promise((resolve, reject) => + target + .readText() + .then(txt => { + dest.mkdir(); + f.writeText(txt).then(_ => resolve(f.readOnly())); + }) + .catch(reject), + ); + saved.set(addr, p); + return p; + }; + + const remove = async () => { + await Promise.all([...saved.values()].map(p => p.then(f => f.unlink()))); + await dest.rmdir(); + }; + + const self = { + toString: () => `${src} -> ${dest}`, + /** @param {string} segment */ + getText: async segment => { + const fr = await getFileP(segment); + return fr.readText(); + }, + /** @param {string} segment */ + storedPath: segment => getFileP(segment).then(f => f.toString()), + /** @param {string} segment */ + size: async segment => { + const fr = await getFileP(segment); + const info = await fr.stat(); + return info.size; + }, + remove, + }; + return self; +}; + +const buildInfo = [ + { + evals: [ + { + permit: 'kread-invite-committee-permit.json', + script: 'kread-invite-committee.js', + }, + ], + bundles: [ + 'b1-51085a4ad4ac3448ccf039c0b54b41bd11e9367dfbd641deda38e614a7f647d7f1c0d34e55ba354d0331b1bf54c999fca911e6a796c90c30869f7fb8887b3024.json', + 'b1-a724453e7bfcaae1843be4532e18c1236c3d6d33bf6c44011f2966e155bc7149b904573014e583fdcde2b9cf2913cb8b337fc9daf79c59a38a37c99030fcf7dc.json', + ], + }, + { + evals: [{ permit: 'start-kread-permit.json', script: 'start-kread.js' }], + bundles: [ + '/Users/wietzes/.agoric/cache/b1-853acd6ba3993f0f19d6c5b0a88c9a722c9b41da17cf7f98ff7705e131860c4737d7faa758ca2120773632dbaf949e4bcce2a2cbf2db224fa09cd165678f64ac.json', + '/Users/wietzes/.agoric/cache/b1-0c3363b8737677076e141a84b84c8499012f6ba79c0871fc906c8be1bb6d11312a7d14d5a3356828a1de6baa4bee818a37b7cb1ca2064f6eecbabc0a40d28136.json', + ], + }, +]; + +const main = async () => { + const td = await new Promise((resolve, reject) => + tmpName({ prefix: 'assets' }, (err, x) => (err ? reject(err) : resolve(x))), + ); + const src = makeWebRd( + 'https://github.com/Kryha/KREAd/releases/download/KREAd-rc1/', + { fetch }, + ); + const fsp = await import('fs/promises'); + const path = await import('path'); + const dest = makeFileRW(td, { fsp, path }); + const assets = makeWebCache(src, dest); + const segment = buildInfo[0].bundles[0]; + const info = await assets.size(segment); + console.log(`${segment}:`, info); +}; + +// main().catch(err => console.error(err));