diff --git a/packages/SwingSet/misc-tools/slog-to-causeway.mjs b/packages/SwingSet/misc-tools/slog-to-causeway.mjs new file mode 100644 index 00000000000..8b6d5e933db --- /dev/null +++ b/packages/SwingSet/misc-tools/slog-to-causeway.mjs @@ -0,0 +1,390 @@ +/** + * TODO: hoist some top level docs + */ + +// @ts-check + +import { pipeline } from 'stream'; + +import './types.js'; + +const { freeze } = Object; + +/** + * @typedef { | + * 'Event' | 'Got' | 'Sent' | 'Resolved' | 'Fulfilled' | 'Rejected' | 'SentIf' | 'Returned' + * } LogClassName + * @typedef { `org.ref_send.log.${LogClassName}` } LogClassT + */ +/** @type { Record } */ +const LogClass = freeze({ + Event: 'org.ref_send.log.Event', + Got: 'org.ref_send.log.Got', + Sent: 'org.ref_send.log.Sent', + // Progressed: 'log.ref_send.Progressed', + Resolved: 'org.ref_send.log.Resolved', + Fulfilled: 'org.ref_send.log.Fulfilled', + Rejected: 'org.ref_send.log.Rejected', + SentIf: 'org.ref_send.log.SentIf', + Returned: 'org.ref_send.log.Returned', +}); + +/** + * uniquely identifies the origin; for example, + * as message send as the 2nd messaging event from the buyer vat, turn 3 + * + * @typedef {{ + * number: number, + * turn: { + * loop: string, // identifies the vat by a unique string + * number: number, + * }, + * }} Anchor + */ + +/** + * @typedef {{ + * anchor: Anchor, + * text?: string, + * trace?: { + * calls: Array<{ + * name: string, + * source: string, + * span: Span, + * }> + * } + * }} TraceRecord + * + * @typedef { TraceRecord & { class: LogClassT[] }} TraceEvent + * + * @typedef { [start: Loc] | [start: Loc, end: Loc] } Span + * @typedef { [line: number] | [line: number, column: number] } Loc + */ + +/** + * An eventual send has two log entries: a `Sent` and its corresponding `Got`. + * + * The timestamp field is optional. Currently, Causeway ignores it. + * + * ref: http://wiki.erights.org/wiki/Causeway_Platform_Developer + * cribbed from https://github.com/cocoonfx/causeway/blob/master/src/js/com/teleometry/causeway/purchase_example/workers/makeCausewayLogger.js + */ +const makeCausewayFormatter = () => { + const self = freeze({ + /** + * @param {Anchor} anchor + * @param {string} message a generated string which uniquely identifies a message + * @param { string } text + */ + makeGot: (anchor, message, text) => + freeze({ + class: [LogClass.Got, LogClass.Event], + anchor, + message, + trace: { calls: [{ name: text, source: '@@' }] }, + }), + /** + * @param {Anchor} anchor + * @param { string } message + * @param { string } text + * @param {LogClassT[]} refinement + */ + makeSent: (anchor, message, text, refinement = []) => + freeze({ + class: [...refinement, LogClass.Sent, LogClass.Event], + anchor, + message, + text, + trace: { calls: [{ name: text, source: '@@' }] }, + }), + /** + * @param {Anchor} anchor + * @param { string } message + * @param { string } text + */ + makeReturned: (anchor, message, text) => + self.makeSent(anchor, message, text, [LogClass.Returned]), + /** + * @param {Anchor} anchor + * @param { string } message + * @param { string } condition + */ + makeSentIf: (anchor, message, condition) => + freeze({ + class: [LogClass.SentIf, LogClass.Sent, LogClass.Event], + anchor, + message, + condition, + // trace: { calls: [{ name: '???@@', source: '@@' }] }, + }), + /** + * @param {Anchor} anchor + * @param { string } condition + * @param {LogClassT[]} status + */ + makeResolved: (anchor, condition, status = []) => + freeze({ + class: [...status, LogClass.Resolved, LogClass.Event], + anchor, + condition, + // trace: { calls: [{ name: '???@@', source: '@@' }] }, + }), + /** + * @param {Anchor} anchor + * @param { string } condition + */ + makeFulfilled: (anchor, condition) => + self.makeResolved(anchor, condition, [LogClass.Fulfilled]), + /** + * @param {Anchor} anchor + * @param { string } condition + */ + makeRejected: (anchor, condition) => + self.makeResolved(anchor, condition, [LogClass.Rejected]), + }); + return self; +}; + +/** + * @param {AsyncIterable} entries + * @yields { TraceEvent } + */ +async function* slogToCauseway(entries) { + const dest = makeCausewayFormatter(); + + /** @type { Map } */ + const vatInfo = new Map(); + + /** @type { (tag: string, obj: Record) => Error } */ + const notImpl = (tag, obj) => + Error(`not implemented: ${tag}: ${JSON.stringify(Object.keys(obj))}`); + /** @type { (result: string, target: string, method: string) => string } */ + // const msgId = (result, target, method) => `${result}<-${target}.${method}`; + + /** + * @param { SlogVatEntry } entry + * @returns { Anchor } + */ + const anchor = entry => { + const { crankNum, vatID, deliveryNum } = entry; + const { name } = vatInfo.get(vatID); + const loop = `${vatID}:${name || '???'}`; + return freeze({ + number: crankNum, + turn: { loop, number: deliveryNum }, + }); + }; + + /** @type { Map } */ + const sent = new Map(); + /** @type { Map } */ + const got = new Map(); + /** @type { Map } */ + const resolved = new Map(); + /** @type { Map } */ + const notified = new Map(); + + for await (const entry of entries) { + switch (entry.type) { + case 'create-vat': + vatInfo.set(entry.vatID, entry); + break; + case 'deliver': { + const { kd } = entry; + switch (kd[0]) { + case 'message': { + const [_tag, _target, { result }] = kd; + got.set(result, entry); + break; + } + case 'notify': { + const [_tag, resolutions] = kd; + for (const [kp] of resolutions) { + notified.set(`R${kp}`, entry); + } + break; + } + case 'retireImports': + case 'retireExports': + case 'dropExports': + break; // ignore + default: + notImpl(kd[0], { kd }); + } + break; + } + case 'syscall': { + switch (entry.ksc[0]) { + case 'send': { + const { + ksc: [_tag, _target, { result }], + } = entry; + sent.set(result, entry); + break; + } + case 'resolve': { + const { + ksc: [_, _thatVat, parts], + } = entry; + for (const [kp, rejected, _args] of parts) { + const { time, crankNum, vatID, deliveryNum } = entry; + resolved.set(`R${kp}`, { + time, + crankNum, + vatID, + deliveryNum, + rejected, + }); + } + break; + } + case 'invoke': + case 'exit': + case 'subscribe': + case 'vatstoreGet': + case 'vatstoreSet': + case 'vatstoreDelete': + case 'vatstoreGetAfter': + case 'dropImports': + case 'retireImports': + case 'retireExports': + break; // irrelevant. ignore. + default: + throw notImpl(entry.ksc[0], {}); + } + break; + } + case 'deliver-result': + case 'import-kernel-start': + case 'import-kernel-finish': + case 'vat-startup-start': + case 'vat-startup-finish': + case 'start-replay': + case 'finish-replay': + case 'start-replay-delivery': + case 'finish-replay-delivery': + case 'cosmic-swingset-begin-block': + case 'cosmic-swingset-end-block-start': + case 'cosmic-swingset-bootstrap-block-start': + case 'cosmic-swingset-bootstrap-block-finish': + case 'cosmic-swingset-end-block-finish': + case 'cosmic-swingset-deliver-inbound': + case 'syscall-result': + case 'clist': + case 'crank-start': + case 'crank-finish': + case 'terminate': + break; // irrelevant. ignore. + case 'console': + break; // TODO: log comment + default: + throw notImpl(entry.type, entry); + } + } + + for (const [kp, src] of sent) { + // send / sent + const { ksc } = src; + if (ksc[0] !== 'send') throw TypeError(); + const [ + _, + ko, + { + method, + args: { body }, + }, + ] = ksc; + const label = `${kp} <- ${ko}.${method}(${body.length})`; + yield dest.makeSent(anchor(src), kp, label); + + // message / got + if (got.has(kp)) { + const target = got.get(kp); + if (target.kd[0] !== 'message') throw TypeError(); + const [_0, t, { method: m }] = target.kd; + yield dest.makeGot(anchor(target), kp, `${t}.${m}(${body.length})`); + } else { + console.warn('no Got for', kp); + // TODO: check for missing data in the other direction? + } + } + + for (const [rkp, src] of resolved) { + // resolve / resolved + const status = src.rejected ? 'rejected' : 'resolved'; + yield dest.makeSent(anchor(src), rkp, `${status} ${rkp}`); + // deliver / returned + if (notified.has(rkp)) { + const target = notified.get(rkp); + yield dest.makeGot(anchor(target), rkp, rkp); + } else { + console.warn('no notified for', rkp); + // TODO: check for missing data in the other direction? + } + } +} + +/** + * TODO: refactor as readLines, map JSON.parse + * + * @param {AsyncIterable} data + * @yields { unknown } + */ +async function* readJSONLines(data) { + let buf = ''; + for await (const chunk of data) { + buf += chunk; + for (let pos = buf.indexOf('\n'); pos >= 0; pos = buf.indexOf('\n')) { + const line = buf.slice(0, pos); + yield JSON.parse(line); + buf = buf.slice(pos + 1); + } + } +} + +/** + * @param { AsyncIterable } items + * @yields { string } + */ +async function* writeJSONArray(items) { + yield '['; + let sep = false; + for await (const item of items) { + if (sep) { + yield ','; + } else { + sep = true; + } + yield '\n'; + yield `${JSON.stringify(item)}`; + } + yield '\n]\n'; +} + +/** + * @param {{ + * stdin: typeof process.stdin, + * stdout: typeof process.stdout, + * }} io + */ +const main = async ({ stdin, stdout }) => + pipeline( + stdin, + readJSONLines, + slogToCauseway, + writeJSONArray, + stdout, + err => { + if (err) throw err; + }, + ); + +/* TODO: only call main() if this is from CLI */ +/* global process */ +main({ + stdin: process.stdin, + stdout: process.stdout, +}).catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/packages/SwingSet/misc-tools/slog-to-diagram.mjs b/packages/SwingSet/misc-tools/slog-to-diagram.mjs new file mode 100644 index 00000000000..466b409a10c --- /dev/null +++ b/packages/SwingSet/misc-tools/slog-to-diagram.mjs @@ -0,0 +1,424 @@ +/* eslint-disable no-continue */ +// @ts-check + +import { pipeline } from 'stream'; + +// [PlantUML \- Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml) + +/** + * TODO: refactor as readLines, map JSON.parse + * + * @param {AsyncIterable} data + * @yields { unknown } + */ +async function* readJSONLines(data) { + let buf = ''; + for await (const chunk of data) { + buf += chunk; + for (let pos = buf.indexOf('\n'); pos >= 0; pos = buf.indexOf('\n')) { + const line = buf.slice(0, pos); + yield JSON.parse(line); + buf = buf.slice(pos + 1); + } + } +} + +/** + * @param {AsyncIterable} entries + * + * @typedef {{time: number}} TimedEvent + * @typedef {{blockTime: number} | DInfo} Arrival + * @typedef {DeliveryInfo | NotifyInfo} DInfo + * @typedef {{ + * type: 'deliver', + * elapsed: number, + * crankNum: number, + * deliveryNum: number, + * vatID: string, + * target: unknown, + * method: string, + * argSize: number, + * compute?: number, + * }} DeliveryInfo + * @typedef {{ + * type: 'deliver', + * elapsed: number, + * vatID: string, + * target: unknown, + * state: unknown, + * compute?: number, + * }} NotifyInfo + * @typedef {{ + * type: 'syscall', + * vatID: string, + * target?: string, + * method?: string, + * } | { + * type: 'resolve', + * vatID: string, + * }} Departure + * + * @typedef {['deliver-result', { + * time: number, type: 'deliver-result', vatID: string + * }]} DeliverResult + */ +async function slogSummary(entries) { + /** @type { Map } */ + const vatInfo = new Map(); + /** @type { Map} */ + const arrival = new Map(); + /** @type { Map} */ + const departure = new Map(); + // track end-of-delivery for deactivating actors + /** @type {DeliverResult[]} */ + const deliverResults = []; + + /** @type {number|undefined} */ + let tBlock; + /** @type {number} */ + let blockHeight; + /** @type { TimedEvent & DInfo | undefined } */ + let dInfo; + + const seen = { + type: new Set(), + deliver: new Set(), + syscall: new Set(), + }; + + for await (const entry of entries) { + // handle off-chain use, such as in unit tests + if (!tBlock) { + tBlock = entry.time; + } + switch (entry.type) { + case 'create-vat': + vatInfo.set(entry.vatID, entry); + break; + case 'cosmic-swingset-end-block-start': + tBlock = entry.time; + blockHeight = entry.blockHeight; + arrival.set(blockHeight, { + time: entry.time, + blockTime: entry.blockTime, + }); + break; + case 'deliver': { + const { kd } = entry; + if (!vatInfo.has(entry.vatID)) + vatInfo.set(entry.vatID, { + type: 'create-vat', + time: entry.time, + vatID: entry.vatID, + }); + switch (kd[0]) { + case 'startVat': { + const [_tag, vatParams] = kd; + dInfo = { + type: entry.type, + time: entry.time, + elapsed: entry.time - tBlock, + crankNum: entry.crankNum, + deliveryNum: entry.deliveryNum, + vatID: entry.vatID, + method: '!startVat', + }; + arrival.set({}, dInfo); + break; + } + case 'message': { + const [ + _tag, + target, + { + methargs: { body }, + result, + }, + ] = kd; + const [method] = JSON.parse(body); + dInfo = { + type: entry.type, + time: entry.time, + elapsed: entry.time - tBlock, + crankNum: entry.crankNum, + deliveryNum: entry.deliveryNum, + vatID: entry.vatID, + target, + method, + argSize: body.length, + }; + arrival.set(result || {}, dInfo); + break; + } + case 'notify': { + const [_tag, resolutions] = kd; + for (const [kp, { state }] of resolutions) { + dInfo = { + type: entry.type, + time: entry.time, + elapsed: entry.time - tBlock, + vatID: entry.vatID, + state, + target: kp, + }; + arrival.set(`R${kp}`, dInfo); + } + break; + } + default: + if (!seen.deliver.has(kd[0])) { + console.warn('delivery tag unknown:', kd[0]); + seen.deliver.add(kd[0]); + } + break; + } + break; + } + case 'deliver-result': { + // track end-of-delivery for deactivating actors + deliverResults.push([ + entry.type, + { time: entry.time, type: entry.type, vatID: entry.vatID }, + ]); + // supplement deliver entry with compute meter + const { dr } = entry; + if (dr[2] && 'compute' in dr[2]) { + if (!dInfo) { + console.warn('no dInfo???', dr[2]); + break; + } + const { compute } = dr[2]; + dInfo.compute = compute; + } + break; + } + case 'syscall': { + switch (entry.ksc[0]) { + case 'send': { + const { + ksc: [_, target, { method, result }], + } = entry; + departure.set(result, { + type: entry.type, + time: entry.time, + vatID: entry.vatID, + target, + method, + }); + break; + } + case 'resolve': { + const { + ksc: [_, _thatVat, parts], + } = entry; + for (const [kp, _rejected, _args] of parts) { + departure.set(`R${kp}`, { + type: entry.type, + time: entry.time, + vatID: entry.vatID, + }); + } + break; + } + default: + if (!seen.syscall.has(entry.ksc[0])) { + console.warn('syscall tag unknown:', entry.ksc[0]); + seen.syscall.add(entry.ksc[0]); + } + // skip + break; + } + break; + } + default: + if (!seen.type.has(entry.type)) { + console.warn('type unknown:', entry.type); + seen.type.add(entry.type); + } + break; + } + } + + return { vatInfo, arrival, departure, deliverResults }; +} + +const { freeze } = Object; + +/** + * ref: https://plantuml.com/sequence-diagram + */ +const fmtPlantUml = freeze({ + /** @param {string} name */ + start: name => `@startuml ${name}\n`, + /** @type {() => string} */ + end: () => '@enduml\n', + /** @type {(l: string, v: string) => string} */ + participant: (label, vatID) => `control ${label} as ${vatID}\n`, + /** @type {(s: string, t: string) => string} */ + note: (side, text) => `note ${side}\n${text}\nend note\n`, + /** @param {string} text */ + delay: text => `... ${text} ...\n`, + /** @param {number} x */ + autonumber: x => `autonumber ${x}\n`, + /** @type {(d: string, msg: string, m?: boolean) => string} */ + incoming: (dest, msg, missing) => + `[${missing ? 'o' : ''}-> ${dest} : ${msg}\n`, + /** @type {(s: string, d: string, msg: string) => string} */ + send: (src, dest, label) => `${src} -> ${dest} : ${label}\n`, + /** @type {(s: string, d: string, msg: string) => string} */ + response: (src, dest, label) => `${src} --> ${dest} : ${label}\n`, +}); + +/** + * ref: https://mermaid-js.github.io/mermaid/ + */ +const fmtMermaid = freeze({ + /** @param {string} _name */ + start: _name => + `\`\`\`mermaid\nsequenceDiagram\n autonumber\n participant Incoming\n`, + end: () => '```\n', + /** @type {(l: string, v: string) => string} */ + participant: (label, vatID) => ` participant ${vatID} as ${label}\n`, + /** @type {(s: string, t: string) => string} */ + note: (side, text) => ` note ${side} of XXXActor ${text}\n`, + /** @param {string} text */ + delay: text => ` %% TODO: delay ... ${text} ...\n`, + /** @param {number} _x */ + autonumber: _x => '', + /** @type {(d: string, msg: string, m?: boolean) => string} */ + incoming: (dest, msg, _missing) => ` Incoming ->> ${dest} : ${msg}\n`, + /** @type {(s: string, d: string, msg: string) => string} */ + send: (src, dest, label) => ` ${src} -) ${dest} : ${label}\n`, + /** @type {(s: string, d: string, msg: string) => string} */ + response: (src, dest, label) => ` ${src} -->> ${dest} : ${label}\n`, +}); + +/** + * @param {typeof fmtPlantUml} fmt + * @param {Awaited>} param0 + * @yields {string} + */ +async function* diagramLines( + fmt, + { vatInfo, arrival, departure, deliverResults }, +) { + yield fmt.start('slog'); + + for (const [vatID, info] of vatInfo) { + const { name = '???' } = info || {}; + const label = JSON.stringify(`${vatID}:${name}`); // stringify to add ""s + yield fmt.participant(label, vatID); + } + + const byTime = [...arrival, ...deliverResults].sort( + (a, b) => a[1].time - b[1].time, + ); + + let active; + let blockHeight; + + for (const [ + ref, + { + type, + elapsed, + vatID: dest, + crankNum, + target, + method, + state, + argSize, + compute, + blockTime, + }, + ] of byTime) { + if (type === 'deliver-result') { + // failed experiment + continue; + } + if (typeof ref === 'number') { + blockHeight = ref; + const dt = new Date(blockTime * 1000).toISOString().slice(0, -5); + yield fmt.delay(`block ${blockHeight} ${dt}`); + continue; + } + const t = Math.round(elapsed * 1000) / 1000; + + // add note to compute-intensive deliveries + // const computeNote = + // compute && compute > 50000 + // ? fmt.note(`right`, `${compute.toLocaleString()} compute`) + // : undefined; + + const computeNote = crankNum + ? fmt.note('right', `${crankNum}@${dest}`) + : undefined; + + // yield `autonumber ${blockHeight}.${t}\n`; + if (t > 0) { + yield fmt.autonumber(t); + } else { + console.warn('??? t < 0', elapsed); + } + const call = `${target}.${method || state}(${argSize || ''})`; + if (typeof ref === 'object') { + yield fmt.incoming(dest, `${call}`); + if (computeNote) yield computeNote; + continue; + } + if (!departure.has(ref)) { + console.warn('no source for', { ref }); + yield fmt.incoming(dest, `${ref} <- ${call}`, true); + if (computeNote) yield computeNote; + continue; + } + const { vatID: src } = departure.get(ref); + const label = method + ? `${ref} <- ${target}.${method}(${argSize || ''})` + : `${target}.${state}()`; + + yield method ? fmt.send(src, dest, label) : fmt.response(src, dest, label); + + // show active vat + if (type === 'deliver' && active !== dest) { + // yield `activate ${dest}\n`; + active = dest; + } + + if (computeNote) yield computeNote; + } + + yield fmt.end(); +} + +/** + * @param {AsyncIterable} entries + * @yields {string} + */ +async function* slogToDiagram(entries) { + const summary = await slogSummary(entries); + for await (const line of diagramLines(fmtPlantUml, summary)) { + yield line; + } +} + +/** + * @param {{ + * stdin: typeof process.stdin, + * stdout: typeof process.stdout, + * }} io + */ +const main = async ({ stdin, stdout }) => + pipeline(stdin, readJSONLines, slogToDiagram, stdout, err => { + if (err) throw err; + }); + +/* TODO: only call main() if this is from CLI */ +/* global process */ +main({ + stdin: process.stdin, + stdout: process.stdout, +}).catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/packages/SwingSet/misc-tools/types.js b/packages/SwingSet/misc-tools/types.js new file mode 100644 index 00000000000..5a342892552 --- /dev/null +++ b/packages/SwingSet/misc-tools/types.js @@ -0,0 +1,87 @@ +/** + * TODO: share types with other swingset sources. + * + * TODO: move delivery-related types and code close together; likewise syscall. + * + * TODO: make ignored stuff less noisy. + * + * @typedef {{ + * time: number + * }} SlogTimedEntry + * @typedef { SlogTimedEntry & { + * crankNum: number, + * vatID: string, + * deliveryNum: number, + * }} SlogVatEntry + * + * @typedef { SlogVatEntry & { + * type: 'deliver', + * kd: KernelDelivery, + * }} SlogDeliveryEntry + * @typedef { | + * [tag: 'message', target: string, msg: Message] | + * [tag: 'notify', resolutions: Array<[ + * kp: string, + * desc: { fulfilled: boolean, refCount: number, data: CapData }, + * ]>] | + * [tag: 'retireImports' | 'retireExports' | 'dropExports'] + * [tag: 'startVat', vatParameters: CapData] + * } KernelDelivery + * @typedef {{ + * method: string, + * args: CapData, + * result: string, + * }} Message + * @typedef {{ + * body: string, + * slots: unknown[], + * }} CapData + * @typedef { SlogVatEntry & { + * type: 'syscall', + * ksc: [tag: 'invoke' | 'subscribe' | 'vatstoreGet'| 'vatstoreSet' | 'vatstoreDelete' | + * 'dropImports' | 'retireImports' | 'retireExports' ] | + * [tag: 'send', target: string, msg: Message] | + * [tag: 'resolve', target: string, + * resolutions: Array<[kp: string, rejected: boolean, value: CapData]>], + * }} SlogSyscallEntry + * + * @typedef { SlogTimedEntry & { + * type: 'create-vat', + * vatID: string, + * dynamic?: boolean, + * description?: string, + * name?: string, + * managerType?: string, + * vatParameters?: Record, + * vatSourceBundle?: unknown, + * }} SlogCreateVatEntry + * @typedef { SlogTimedEntry & { + * type: 'cosmic-swingset-end-block-start', + * blockHeight: number, + * blockTime: number, + * }} SlogEndBlockStartEntry + * @typedef { SlogTimedEntry & { + * type: 'deliver-result', + * vatID: string, + * dr: [tag: unknown, x: unknown, meter: {}], + * }} SlogDeliverResultEntry + * @typedef { SlogTimedEntry & { + * type: 'import-kernel-start' | 'import-kernel-finish' + * | 'vat-startup-start' | 'vat-startup-finish' + * | 'start-replay' | 'finish-replay' + * | 'start-replay-delivery' | 'finish-replay-delivery' + * | 'cosmic-swingset-begin-block' + * | 'cosmic-swingset-end-block-finish' + * | 'cosmic-swingset-deliver-inbound' + * | 'syscall-result' + * | 'clist' + * | 'crank-start' | 'crank-finish' + * | 'console' + * | '@@more TODO' + * }} SlogToDoEntry + * @typedef {| + * SlogDeliveryEntry | SlogSyscallEntry | SlogCreateVatEntry | + * SlogEndBlockStartEntry| SlogDeliverResultEntry | + * SlogToDoEntry + * } SlogEntry + */ diff --git a/packages/cosmic-swingset/Makefile b/packages/cosmic-swingset/Makefile index 7716821c0a6..d6efaf6177d 100644 --- a/packages/cosmic-swingset/Makefile +++ b/packages/cosmic-swingset/Makefile @@ -144,6 +144,11 @@ scenario2-run-chain-economy: t1/decentral-economy-config.json OTEL_EXPORTER_PROMETHEUS_PORT=$(OTEL_EXPORTER_PROMETHEUS_PORT) \ $(AGC) --home=t1/n0 start --log_level=warn $(AGC_START_ARGS) +scenario2-run-chain-psm: ../vats/decentral-psm-config.json + CHAIN_BOOTSTRAP_VAT_CONFIG="$$PWD/../vats/decentral-psm-config.json" \ + OTEL_EXPORTER_PROMETHEUS_PORT=$(OTEL_EXPORTER_PROMETHEUS_PORT) \ + $(AGC) --home=t1/n0 start --log_level=warn $(AGC_START_ARGS) + scenario2-run-chain: OTEL_EXPORTER_PROMETHEUS_PORT=$(OTEL_EXPORTER_PROMETHEUS_PORT) \ $(AGC) --home=t1/n0 start --log_level=warn $(AGC_START_ARGS) diff --git a/packages/inter-protocol/scripts/build-bundles.js b/packages/inter-protocol/scripts/build-bundles.js index 9c4ac34a37b..203b875f59d 100644 --- a/packages/inter-protocol/scripts/build-bundles.js +++ b/packages/inter-protocol/scripts/build-bundles.js @@ -1,6 +1,9 @@ #! /usr/bin/env node import '@endo/init'; -import { extractProposalBundles } from '@agoric/deploy-script-support'; +import { + createBundles, + extractProposalBundles, +} from '@agoric/deploy-script-support'; import url from 'url'; import process from 'process'; @@ -18,3 +21,8 @@ await extractProposalBundles( ], dirname, ); + +await createBundles( + [['../src/psm/psm.js', '../bundles/bundle-psm.js']], + dirname, +); diff --git a/packages/inter-protocol/src/proposals/core-proposal.js b/packages/inter-protocol/src/proposals/core-proposal.js index 1d943036f7e..f04116ce548 100644 --- a/packages/inter-protocol/src/proposals/core-proposal.js +++ b/packages/inter-protocol/src/proposals/core-proposal.js @@ -9,7 +9,7 @@ export * from './sim-behaviors.js'; // named 'EconomyBootstrapPowers'. export * from './startPSM.js'; -const ECON_COMMITTEE_MANIFEST = harden({ +export const ECON_COMMITTEE_MANIFEST = harden({ [econBehaviors.startEconomicCommittee.name]: { consume: { zoe: true, diff --git a/packages/inter-protocol/src/proposals/startPSM.js b/packages/inter-protocol/src/proposals/startPSM.js index 7e9dd011e57..fdbb23bb420 100644 --- a/packages/inter-protocol/src/proposals/startPSM.js +++ b/packages/inter-protocol/src/proposals/startPSM.js @@ -216,6 +216,28 @@ export const makeAnchorAsset = async ( }; harden(makeAnchorAsset); +/** @param {BootstrapSpace & { devices: { vatAdmin: any }, vatPowers: { D: DProxy }, }} powers */ +export const installGovAndPSMContracts = async ({ + consume: { zoe, vatAdminSvc }, + installation: { + produce: { contractGovernor, committee, binaryVoteCounter, psm }, + }, +}) => { + return Promise.all( + Object.entries({ + contractGovernor, + committee, + binaryVoteCounter, + psm, + }).map(async ([name, producer]) => { + const bundleID = await E(vatAdminSvc).getBundleIDByName(name); + + const installation = E(zoe).installBundleID(bundleID); + producer.resolve(installation); + }), + ); +}; + export const PSM_MANIFEST = harden({ [makeAnchorAsset.name]: { consume: { agoricNamesAdmin: true, bankManager: 'bank', zoe: 'zoe' }, @@ -252,6 +274,17 @@ export const PSM_MANIFEST = harden({ consume: { AUSD: 'bank' }, }, }, + [installGovAndPSMContracts.name]: { + consume: { zoe: 'zoe', vatAdminSvc: true }, + installation: { + produce: { + contractGovernor: 'zoe', + committee: 'zoe', + binaryVoteCounter: 'zoe', + psm: 'zoe', + }, + }, + }, }); export const getManifestForPsm = ( diff --git a/packages/vats/decentral-psm-config.json b/packages/vats/decentral-psm-config.json new file mode 100644 index 00000000000..e00fd49765e --- /dev/null +++ b/packages/vats/decentral-psm-config.json @@ -0,0 +1,56 @@ +{ + "bootstrap": "bootstrap", + "defaultReapInterval": 1000, + "vats": { + "bootstrap": { + "sourceSpec": "@agoric/vats/src/core/boot-psm.js", + "parameters": { + "anchorAssets": [ + { + "denom": "ibc/usdc1234" + } + ], + "economicCommitteeAddresses": [] + } + } + }, + "bundles": { + "walletFactory": { + "sourceSpec": "@agoric/wallet/contract/src/walletFactory.js" + }, + "committee": { + "sourceSpec": "@agoric/governance/src/committee.js" + }, + "contractGovernor": { + "sourceSpec": "@agoric/governance/src/contractGovernor.js" + }, + "binaryVoteCounter": { + "sourceSpec": "@agoric/governance/src/binaryVoteCounter.js" + }, + "psm": { + "sourceSpec": "@agoric/inter-protocol/src/psm/psm.js" + }, + "bank": { + "sourceSpec": "@agoric/vats/src/vat-bank.js" + }, + "centralSupply": { + "sourceSpec": "@agoric/vats/src/centralSupply.js" + }, + "mintHolder": { + "sourceSpec": "@agoric/vats/src/mintHolder.js" + }, + "board": { + "sourceSpec": "@agoric/vats/src/vat-board.js" + }, + "chainStorage": { + "sourceSpec": "@agoric/vats/src/vat-chainStorage.js" + }, + "zcf": { + "sourceSpec": "@agoric/zoe/contractFacet.js" + }, + "zoe": { + "sourceSpec": "@agoric/vats/src/vat-zoe.js" + } + }, + "defaultManagerType": "xs-worker" +} diff --git a/packages/vats/src/core/basic-behaviors.js b/packages/vats/src/core/basic-behaviors.js index c1840e4cb5e..fb9ff888cb9 100644 --- a/packages/vats/src/core/basic-behaviors.js +++ b/packages/vats/src/core/basic-behaviors.js @@ -22,7 +22,7 @@ const RESERVE_ADDRESS = 'agoric1ae0lmtzlgrcnla9xjkpaarq5d5dfez63h3nucl'; * non-exhaustive list of powerFlags * REMOTE_WALLET is currently a default. */ -const PowerFlags = /** @type {const} */ ({ +export const PowerFlags = /** @type {const} */ ({ SMART_WALLET: 'SMART_WALLET', /** The ag-solo wallet is remote. */ REMOTE_WALLET: 'REMOTE_WALLET', @@ -173,6 +173,28 @@ export const makeBoard = async ({ }; harden(makeBoard); +/** + * @param {NameAdmin} namesByAddressAdmin + * @param {string} address + */ +export const makeMyAddressNameAdmin = (namesByAddressAdmin, address) => { + // Create a name hub for this address. + const { nameHub: myAddressNameHub, nameAdmin: rawMyAddressNameAdmin } = + makeNameHubKit(); + + /** @type {MyAddressNameAdmin} */ + const myAddressNameAdmin = Far('myAddressNameAdmin', { + ...rawMyAddressNameAdmin, + getMyAddress: () => address, + }); + // reserve space for deposit facet + myAddressNameAdmin.reserve('depositFacet'); + // Register it with the namesByAddress hub. + namesByAddressAdmin.update(address, myAddressNameHub, myAddressNameAdmin); + + return myAddressNameAdmin; +}; + /** * Make the agoricNames, namesByAddress name hierarchies. * @@ -200,19 +222,10 @@ export const makeAddressNameHubs = async ({ produce.namesByAddressAdmin.resolve(namesByAddressAdmin); const perAddress = address => { - // Create a name hub for this address. - const { nameHub: myAddressNameHub, nameAdmin: rawMyAddressNameAdmin } = - makeNameHubKit(); - - /** @type {MyAddressNameAdmin} */ - const myAddressNameAdmin = Far('myAddressNameAdmin', { - ...rawMyAddressNameAdmin, - getMyAddress: () => address, - }); - // reserve space for deposit facet - myAddressNameAdmin.reserve('depositFacet'); - // Register it with the namesByAddress hub. - namesByAddressAdmin.update(address, myAddressNameHub, myAddressNameAdmin); + const myAddressNameAdmin = makeMyAddressNameAdmin( + namesByAddressAdmin, + address, + ); return { agoricNames, namesByAddress, myAddressNameAdmin }; }; diff --git a/packages/vats/src/core/boot-psm.js b/packages/vats/src/core/boot-psm.js new file mode 100644 index 00000000000..31e9e3d6ed7 --- /dev/null +++ b/packages/vats/src/core/boot-psm.js @@ -0,0 +1,187 @@ +// @ts-check +import { Far } from '@endo/far'; +import { + installGovAndPSMContracts, + makeAnchorAsset, + startPSM, + PSM_MANIFEST, +} from '@agoric/inter-protocol/src/proposals/startPSM.js'; +// TODO: factor startEconomicCommittee out of econ-behaviors.js +import { startEconomicCommittee } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; +import { ECON_COMMITTEE_MANIFEST } from '@agoric/inter-protocol/src/proposals/core-proposal.js'; +import { makeAgoricNamesAccess, makePromiseSpace } from './utils.js'; +import { Stable, Stake } from '../tokens.js'; +import { + addBankAssets, + buildZoe, + installBootContracts, + makeAddressNameHubs, + makeBoard, + makeVatsFromBundles, + mintInitialSupply, +} from './basic-behaviors.js'; +import * as utils from './utils.js'; +import { + makeBridgeManager, + makeChainStorage, + publishAgoricNames, + startTimerService, +} from './chain-behaviors.js'; +import { CHAIN_BOOTSTRAP_MANIFEST } from './manifest.js'; +import { startWalletFactory } from './startWalletFactory.js'; + +/** @typedef {import('@agoric/inter-protocol/src/proposals/econ-behaviors.js').EconomyBootstrapSpace} EconomyBootstrapSpace */ + +/** + * We reserve these keys in name hubs. + */ +export const agoricNamesReserved = harden( + /** @type {const} */ ({ + issuer: { + [Stake.symbol]: Stake.proposedName, + [Stable.symbol]: Stable.proposedName, + AUSD: 'Agoric bridged USDC', + }, + brand: { + [Stake.symbol]: Stake.proposedName, + [Stable.symbol]: Stable.proposedName, + AUSD: 'Agoric bridged USDC', + }, + installation: { + centralSupply: 'central supply', + mintHolder: 'mint holder', + walletFactory: 'multitenant smart wallet', + contractGovernor: 'contract governor', + committee: 'committee electorate', + binaryVoteCounter: 'binary vote counter', + psm: 'Parity Stability Module', + }, + instance: { + economicCommittee: 'Economic Committee', + psm: 'Parity Stability Module', + psmGovernor: 'PSM Governor', + }, + }), +); + +/** + * Build root object of the PSM-only bootstrap vat. + * + * @param {{ + * D: DProxy + * }} vatPowers + * @param {{ + * economicCommitteeAddresses: string[], + * anchorAssets: { denom: string }[], + * }} vatParameters + */ +export const buildRootObject = (vatPowers, vatParameters) => { + // @ts-expect-error no TS defs for rickety test scaffolding + const log = vatPowers.logger || console.info; + + const { + anchorAssets: [{ denom: anchorDenom }], // TODO: handle >1? + economicCommitteeAddresses, + } = vatParameters; + + const { produce, consume } = makePromiseSpace(log); + const { agoricNames, agoricNamesAdmin, spaces } = makeAgoricNamesAccess( + log, + agoricNamesReserved, + ); + produce.agoricNames.resolve(agoricNames); + produce.agoricNamesAdmin.resolve(agoricNamesAdmin); + + const runBootstrapParts = async (vats, devices) => { + /** TODO: BootstrapPowers type puzzle */ + /** @type { any } */ + const allPowers = harden({ + vatPowers, + vatParameters, + vats, + devices, + produce, + consume, + ...spaces, + // ISSUE: needed? runBehaviors, + // These module namespaces might be useful for core eval governance. + modules: { + utils: { ...utils }, + }, + }); + const manifest = { + ...CHAIN_BOOTSTRAP_MANIFEST, + ...ECON_COMMITTEE_MANIFEST, + ...PSM_MANIFEST, + }; + const powersFor = name => { + const permit = manifest[name]; + assert(permit, `missing permit for ${name}`); + return utils.extractPowers(permit, allPowers); + }; + + await Promise.all([ + makeVatsFromBundles(powersFor('makeVatsFromBundles')), + buildZoe(powersFor('buildZoe')), + makeBoard(powersFor('makeBoard')), + makeBridgeManager(powersFor('makeBridgeManager')), + makeChainStorage(powersFor('makeChainStorage')), + makeAddressNameHubs(powersFor('makeAddressNameHubs')), + publishAgoricNames(powersFor('publishAgoricNames'), { + options: { + agoricNamesOptions: { topLevel: Object.keys(agoricNamesReserved) }, + }, + }), + startWalletFactory(powersFor('startWalletFactory')), + mintInitialSupply(powersFor('mintInitialSupply')), + addBankAssets(powersFor('addBankAssets')), + startTimerService(powersFor('startTimerService')), + // centralSupply, mintHolder, walletFactory + installBootContracts(powersFor('installBootContracts')), + installGovAndPSMContracts(powersFor('installGovAndPSMContracts')), + startEconomicCommittee(powersFor('startEconomicCommittee'), { + options: { + econCommitteeOptions: { + committeeSize: economicCommitteeAddresses.length, + }, + }, + }), + makeAnchorAsset(powersFor('makeAnchorAsset'), { + options: { anchorOptions: { denom: anchorDenom } }, + }), + startPSM(powersFor('startPSM'), { + options: { anchorOptions: { denom: anchorDenom } }, + }), + ]); + }; + + return Far('bootstrap', { + bootstrap: (vats, devices) => + runBootstrapParts(vats, devices).catch(e => { + console.error('BOOTSTRAP FAILED:', e); + throw e; + }), + /** + * Allow kernel to provide things to CORE_EVAL. + * + * @param {string} name + * @param {unknown} resolution + */ + produceItem: (name, resolution) => { + assert.typeof(name, 'string'); + produce[name].resolve(resolution); + }, + // expose reset in case we need to do-over + resetItem: name => { + assert.typeof(name, 'string'); + produce[name].reset(); + }, + // expose consume mostly for testing + consumeItem: name => { + assert.typeof(name, 'string'); + return consume[name]; + }, + }); +}; + +harden({ buildRootObject }); diff --git a/packages/vats/src/core/chain-behaviors.js b/packages/vats/src/core/chain-behaviors.js index d5bc221f797..dd65bcf2404 100644 --- a/packages/vats/src/core/chain-behaviors.js +++ b/packages/vats/src/core/chain-behaviors.js @@ -317,10 +317,16 @@ export const makeChainStorage = async ({ chainStorageP.resolve(rootNodeP); }; -/** @param {BootstrapPowers} powers */ -export const publishAgoricNames = async ({ - consume: { agoricNamesAdmin, board, chainStorage: rootP }, -}) => { +/** + * @param {BootstrapPowers} powers + * @param {{ options?: {agoricNamesOptions?: { + * topLevel?: string[] + * }}}} config + */ +export const publishAgoricNames = async ( + { consume: { agoricNamesAdmin, board, chainStorage: rootP } }, + { options: { agoricNamesOptions } = {} } = {}, +) => { const root = await rootP; if (!root) { console.warn('cannot publish agoricNames without chainStorage'); @@ -330,8 +336,9 @@ export const publishAgoricNames = async ({ const marshaller = E(board).getPublishingMarshaller(); // brand, issuer, ... + const { topLevel = keys(agoricNamesReserved) } = agoricNamesOptions || {}; await Promise.all( - keys(agoricNamesReserved).map(async kind => { + topLevel.map(async kind => { const kindAdmin = await E(agoricNamesAdmin).lookupAdmin(kind); const kindNode = await E(nameStorage).makeChildNode(kind); diff --git a/packages/vats/src/core/manifest.js b/packages/vats/src/core/manifest.js index 9d8e1e36a94..64aeeb2137e 100644 --- a/packages/vats/src/core/manifest.js +++ b/packages/vats/src/core/manifest.js @@ -124,6 +124,25 @@ const SHARED_CHAIN_BOOTSTRAP_MANIFEST = harden({ installation: { consume: { walletFactory: 'zoe' } }, home: { produce: { bank: 'bank' } }, }, + startWalletFactory: { + consume: { + agoricNames: true, + bankManager: 'bank', + board: 'board', + bridgeManager: true, + chainStorage: 'chainStorage', + namesByAddress: true, + namesByAddressAdmin: true, + zoe: 'zoe', + }, + produce: { + client: true, // dummy client in this configuration + smartWalletStartResult: true, + }, + installation: { + consume: { walletFactory: 'zoe' }, + }, + }, installBootContracts: { vatPowers: { D: true }, devices: { vatAdmin: true }, diff --git a/packages/vats/src/core/startWalletFactory.js b/packages/vats/src/core/startWalletFactory.js new file mode 100644 index 00000000000..d06093c715e --- /dev/null +++ b/packages/vats/src/core/startWalletFactory.js @@ -0,0 +1,104 @@ +// @ts-check +import { E, Far } from '@endo/far'; +import { deeplyFulfilled } from '@endo/marshal'; + +import * as BRIDGE_ID from '../bridge-ids.js'; +import { makeStorageNodeChild } from '../lib-chainStorage.js'; +import { makeMyAddressNameAdmin, PowerFlags } from './basic-behaviors.js'; + +const { details: X } = assert; + +/** + * Register for PLEASE_PROVISION bridge messages and handle + * them by providing a smart wallet from the wallet factory. + * + * @param {BootstrapPowers} param0 + */ +export const startWalletFactory = async ({ + consume: { + agoricNames, + bankManager, + board, + bridgeManager: bridgeManagerP, + chainStorage, + namesByAddress, + namesByAddressAdmin: namesByAddressAdminP, + zoe, + }, + produce: { client, smartWalletStartResult }, + installation: { + consume: { walletFactory }, + }, +}) => { + const STORAGE_PATH = 'wallet'; + + const [storageNode, bridgeManager, namesByAddressAdmin] = await Promise.all([ + makeStorageNodeChild(chainStorage, STORAGE_PATH), + bridgeManagerP, + namesByAddressAdminP, + ]); + + const terms = await deeplyFulfilled( + harden({ + agoricNames, + namesByAddress, + board, + }), + ); + const x = await E(zoe).startInstance(walletFactory, {}, terms, { + storageNode, + bridgeManager, + }); + smartWalletStartResult.resolve(x); + const { creatorFacet } = x; + + /** @param {string} address */ + const tryLookup = async address => + E(namesByAddress) + .lookup(address) + .catch(_notFound => undefined); + + const handler = Far('provisioningHandler', { + fromBridge: async (_srcID, obj) => { + assert.equal( + obj.type, + 'PLEASE_PROVISION', + X`Unrecognized request ${obj.type}`, + ); + const { address, powerFlags } = obj; + console.info('PLEASE_PROVISION', address, powerFlags); + + const hubBefore = await tryLookup(address); + assert(!hubBefore, 'already provisioned'); + + if (!powerFlags.includes(PowerFlags.SMART_WALLET)) { + return; + } + const myAddressNameAdmin = makeMyAddressNameAdmin( + namesByAddressAdmin, + address, + ); + + const bank = E(bankManager).getBankForAddress(address); + await E(creatorFacet).provideSmartWallet( + address, + bank, + myAddressNameAdmin, + ); + console.info('provisioned', address, powerFlags); + }, + }); + if (!bridgeManager) { + console.warn('missing bridgeManager in startWalletFactory'); + } + await (bridgeManager && + E(bridgeManager).register(BRIDGE_ID.PROVISION, handler)); + + client.resolve( + Far('dummy client', { + assignBundle: (propertyMakers = []) => { + console.warn('ignoring', propertyMakers.length, 'propertyMakers'); + }, + }), + ); +}; diff --git a/packages/vats/test/devices.js b/packages/vats/test/devices.js index ee29987f8a0..88c14f9f45a 100644 --- a/packages/vats/test/devices.js +++ b/packages/vats/test/devices.js @@ -1,24 +1,31 @@ +import bundleCommittee from '@agoric/governance/bundles/bundle-committee.js'; +import bundleContractGovernor from '@agoric/governance/bundles/bundle-contractGovernor.js'; +import bundleBinaryVoteCounter from '@agoric/governance/bundles/bundle-binaryVoteCounter.js'; +import bundlePSM from '@agoric/inter-protocol/bundles/bundle-psm.js'; + import bundleCentralSupply from '../bundles/bundle-centralSupply.js'; import bundleMintHolder from '../bundles/bundle-mintHolder.js'; import bundleSingleWallet from '../bundles/bundle-singleWallet.js'; import bundleWalletFactory from '../bundles/bundle-walletFactory.js'; +const bundles = { + centralSupply: bundleCentralSupply, + mintHolder: bundleMintHolder, + singleWallet: bundleSingleWallet, + walletFactory: bundleWalletFactory, + committee: bundleCommittee, + contractGovernor: bundleContractGovernor, + binaryVoteCounter: bundleBinaryVoteCounter, + psm: bundlePSM, +}; + export const devices = { vatAdmin: { getNamedBundleCap: name => ({ getBundle: () => { - switch (name) { - case 'centralSupply': - return bundleCentralSupply; - case 'mintHolder': - return bundleMintHolder; - case 'singleWallet': - return bundleSingleWallet; - case 'walletFactory': - return bundleWalletFactory; - default: - throw new Error(`unknown bundle ${name}`); - } + const bundle = bundles[name]; + assert(bundle, `unknown bundle ${name}`); + return bundle; }, }), }, diff --git a/packages/vats/test/test-boot.js b/packages/vats/test/test-boot.js index fb5c012abc1..cf57c8a85ae 100644 --- a/packages/vats/test/test-boot.js +++ b/packages/vats/test/test-boot.js @@ -1,7 +1,7 @@ // @ts-check // eslint-disable-next-line import/no-extraneous-dependencies import { test } from '@agoric/swingset-vat/tools/prepare-test-env-ava.js'; -import { E, Far } from '@endo/far'; +import { E, Far, passStyleOf } from '@endo/far'; import bundleSource from '@endo/bundle-source'; import { makeFakeVatAdmin, @@ -11,6 +11,7 @@ import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { makeZoeKit } from '@agoric/zoe'; import { buildRootObject } from '../src/core/boot.js'; +import { buildRootObject as buildPSMRootObject } from '../src/core/boot-psm.js'; import { bridgeCoreEval } from '../src/core/chain-behaviors.js'; import { makePromiseSpace } from '../src/core/utils.js'; import { buildRootObject as bankRoot } from '../src/vat-bank.js'; @@ -79,6 +80,61 @@ const makeMock = t => }, }); +const mockSwingsetVats = mock => { + const { admin: fakeVatAdmin } = makeFakeVatAdmin(() => {}); + const fakeBundleCaps = new Map(); // {} -> name + const getNamedBundleCap = name => { + const bundleCap = harden({}); + fakeBundleCaps.set(bundleCap, name); + return bundleCap; + }; + const getBundleIDByName = name => `b1-${name}`; + const getBundleCap = id => devices.vatAdmin.getBundleCap(id); + + const createVat = (bundleCap, options) => { + const name = fakeBundleCaps.get(bundleCap); + assert(name); + switch (name) { + case 'zcf': + return fakeVatAdmin.createVat(zcfBundleCap, options); + default: { + const buildRoot = vatRoots[name]; + if (!buildRoot) { + throw Error(`TODO: load vat ${name}`); + } + const vatParameters = { ...options?.vatParameters }; + if (name === 'zoe') { + // basic-behaviors.js:buildZoe() provides hard-coded zcf BundleName + // and vat-zoe.js ignores vatParameters, but this would be the + // preferred way to pass the name. + vatParameters.zcfBundleName = 'zcf'; + } + return { root: buildRoot({}, vatParameters), admin: {} }; + } + } + }; + const createVatByName = name => { + const bundleCap = getNamedBundleCap(name); + return createVat(bundleCap); + }; + + const vats = { + ...mock.vats, + vatAdmin: /** @type { any } */ ({ + createVatAdminService: () => + Far('vatAdminSvc', { + getNamedBundleCap, + createVat, + createVatByName, + getBundleIDByName, + getBundleCap, + waitForBundleCap: getBundleCap, + }), + }), + }; + return vats; +}; + const argvByRole = { chain: { ROLE: 'chain', @@ -230,3 +286,40 @@ test('bootstrap provides a way to pass items to CORE_EVAL', async t => { await E(root).produceItem('swissArmyKnife', 4); t.deepEqual(await E(root).consumeItem('swissArmyKnife'), 4); }); + +const psmParams = { + anchorAssets: [{ denom: 'ibc/toyusdc' }], + economicCommitteeAddresses: [], + argv: { bootMsg: {} }, +}; + +test(`PSM-only bootstrap`, async t => { + const mock = makeMock(t); + const root = buildPSMRootObject( + // @ts-expect-error Device is a little goofy + { D: d => d, logger: t.log }, + psmParams, + ); + + const vats = mockSwingsetVats(mock); + const actual = await E(root).bootstrap(vats, mock.devices); + t.deepEqual(actual, undefined); + + const agoricNames = E(root).consumeItem('agoricNames'); + const instance = await E(agoricNames).lookup('instance', 'psm'); + t.is(passStyleOf(instance), 'remotable'); +}); + +test('PSM-only bootstrap provides a way to pass items to CORE_EVAL', async t => { + const root = buildPSMRootObject( + // @ts-expect-error Device is a little goofy + { D: d => d, logger: t.log }, + psmParams, + ); + + await E(root).produceItem('swissArmyKnife', [1, 2, 3]); + t.deepEqual(await E(root).consumeItem('swissArmyKnife'), [1, 2, 3]); + await E(root).resetItem('swissArmyKnife'); + await E(root).produceItem('swissArmyKnife', 4); + t.deepEqual(await E(root).consumeItem('swissArmyKnife'), 4); +});