From 2c189c6d9d285327eeef3534745c8f75c02c1600 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Mon, 6 Nov 2023 10:11:02 -0500 Subject: [PATCH] feat(SwingSet): Add a tool for classifying unsettled promises Ref #8336 --- .../SwingSet/misc-tools/classify-promises.js | 478 ++++++++++++++++++ packages/kmarshal/src/kmarshal.js | 2 + 2 files changed, 480 insertions(+) create mode 100755 packages/SwingSet/misc-tools/classify-promises.js diff --git a/packages/SwingSet/misc-tools/classify-promises.js b/packages/SwingSet/misc-tools/classify-promises.js new file mode 100755 index 000000000000..4a31d9baf192 --- /dev/null +++ b/packages/SwingSet/misc-tools/classify-promises.js @@ -0,0 +1,478 @@ +#!/usr/bin/env node +/* eslint no-labels: "off", no-extra-label: "off", no-underscore-dangle: "off" */ +import process from 'process'; +import sqlite3 from 'better-sqlite3'; +import yargsParser from 'yargs-parser'; +import '@endo/init/debug.js'; +import { makeStandinPromise, krefOf } from '@agoric/kmarshal'; +import { Far } from '@endo/far'; +import { makeMarshal } from '@endo/marshal'; +import { M, matches, mustMatch } from '@endo/patterns'; + +const EX_USAGE = 64; +const { + WALLET_FACTORY_VAT_ID = 'v43', + ZOE_VAT_ID = 'v9', + VAT_ADMIN_SERVICE_KREF = 'ko25', + ZOE_SERVICE_KREF = 'ko65', +} = process.env; +const VAT_NAME_PATTERNS = { + governor: /^zcf-b1-9f877-(?.*)[.-]governor$/, + mintHolder: /^zcf-mintHolder-(?.*)$/, + psm: /^zcf-b1-c25fb-psm-(?.*)$/, + voteCounter: /^zcf-b1-78999-voteCounter[.](?.*)$/, +}; + +// CapData decoding that exposes slot data (e.g., promise/remotable krefs). +const { toCapData, fromCapData, getPresence } = (() => { + const presences = new Map(); + // eslint-disable-next-line no-shadow + const getPresence = kref => { + const p = presences.get(kref); + assert(p !== undefined); + return p; + }; + // cf. kunser + const slotToPresence = (kref, iface = 'undefined') => { + const found = presences.get(kref); + if (found) return found; + + assert.typeof(kref, 'string'); + const p = kref.match(/^[klr]?p/) + ? makeStandinPromise(kref) + : Far(iface.replace(/^Alleged: /, ''), { + iface: () => iface, + getKref: () => kref, + }); + presences.set(kref, p); + return p; + }; + // eslint-disable-next-line no-shadow + const { toCapData, fromCapData } = makeMarshal(undefined, slotToPresence, { + serializeBodyFormat: 'smallcaps', + }); + return { toCapData, fromCapData, getPresence }; +})(); +const describeCapData = capData => { + const data = fromCapData(capData); + const { slots } = capData; + const presences = slots.map(kref => getPresence(kref)); + return harden({ data, slots, presences }); +}; + +const capDataShape = M.splitRecord({ slots: M.array() }); +const makeSyscallShape = type => + M.splitRecord({ type, vatID: M.string(), ksc_json: M.string() }); +/** + * @param {PropertyKey} methodName + * @param {unknown[] | import('@endo/patterns').Matcher} argsShape + * @returns {import('@endo/patterns').Pattern} + */ +const makeMethargsShape = (methodName, argsShape = M.arrayOf(M.any())) => + harden([methodName, argsShape]); + +// A syscall sequence of minimum length 2 in which the first is a send and it is +// followed by a subscribe (presumably to the result of the first). +const sendAndSubscribeShape = M.splitArray([ + makeSyscallShape('send'), + makeSyscallShape('subscribe'), +]); + +// syscall.send: ['send', targetKref, message: { methargs: capData, result?: kpid }] +const kscSendShape = harden([ + 'send', + M.string(), + M.splitRecord({ methargs: capDataShape }, { result: M.string() }), +]); +const decodeSendSyscall = syscall => { + const ksc = harden(JSON.parse(syscall.ksc_json)); + mustMatch(ksc, kscSendShape); + const [_syscallType, targetKref, message] = ksc; + const { + data: methargs, + slots: methargsSlots, + presences: methargsPresences, + } = describeCapData(message.methargs); + const { result: resultKpid } = message; + return harden({ + sourceVatID: syscall.vatID, + targetKref, + methargs, + methargsSlots, + methargsPresences, + resultKpid, + }); +}; + +// syscall.subscribe: ['subscribe', vatID, kpid] +const kscSubscribeShape = harden(['subscribe', M.string(), M.string()]); +const decodeSubscribeSyscall = syscall => { + const ksc = harden(JSON.parse(syscall.ksc_json)); + mustMatch(ksc, kscSubscribeShape); + const [_syscallType, _vatID, kpid] = ksc; + return harden({ sourceVatID: syscall.vatID, kpid }); +}; + +// syscall.resolve: ['resolve', vatID, settlements: Array<[kpid, isRejection, capData]>] +const kscResolveShape = harden([ + 'resolve', + M.string(), + M.arrayOf([M.string(), M.boolean(), capDataShape]), +]); +const decodeResolveSyscall = syscall => { + const ksc = harden(JSON.parse(syscall.ksc_json)); + mustMatch(ksc, kscResolveShape); + const [_syscallType, _vatID, encodedSettlements] = ksc; + const settlements = encodedSettlements.map(([kpid, isRejection, capData]) => { + const { data, slots, presences } = describeCapData(capData); + return { kpid, isRejection, data, slots, presences }; + }); + return harden({ sourceVatID: syscall.vatID, settlements }); +}; + +/** + * @typedef {object} SyscallTraceData + * @property {Array} krefHistory + * @property {Array>} syscalls + */ +/** @typedef {SyscallTraceData & {failure: string}} SyscallTraceFailure */ +/** @typedef {SyscallTraceData & {useRecord?: ReturnType, request: ReturnType}} SyscallTraceSuccess */ + +/** + * @param {import('better-sqlite3').Statement<[kpid: string]>} getSyscallsForKref + * @param {string} kref + * @returns {SyscallTraceFailure | SyscallTraceSuccess} + */ +const traceSyscalls = (getSyscallsForKref, kref) => { + const krefHistory = [kref]; + let syscalls = []; + let useRecord; + /** @type {string | undefined} */ + let pendingKref = kref; + nextKref: while (pendingKref) { + const currentKref = pendingKref; + pendingKref = undefined; + const moreSyscalls = getSyscallsForKref.all(currentKref); + for (let i = 0; i < moreSyscalls.length; i += 1) { + const syscall = moreSyscalls[i]; + const makeResult = fields => ({ + ...fields, + krefHistory, + syscalls: [...moreSyscalls.slice(0, i + 1), ...syscalls], + }); + if (syscall.type === 'send') { + const decoded = decodeSendSyscall(syscall); + if (decoded.resultKpid === currentKref) { + // End of the line! + return harden(makeResult({ useRecord, request: decoded })); + } else if (decoded.methargsSlots.includes(currentKref)) { + if (useRecord) { + return harden( + makeResult({ + failure: `multiple syscall bodies relating to ${kref}/${currentKref} at syscall ${i}`, + }), + ); + } + useRecord = { + data: decoded.methargs, + slots: decoded.methargsSlots, + presences: decoded.methargsPresences, + }; + continue; + } + return harden(makeResult({ failure: `unexpected syscall.send` })); + } else if (syscall.type === 'resolve') { + const decoded = decodeResolveSyscall(syscall); + const settlement = decoded.settlements.find( + ({ kpid, slots }) => + kpid === currentKref || slots.includes(currentKref), + ); + if (settlement.kpid === currentKref) { + if (settlement.slots.length !== 1) { + return harden( + makeResult({ + failure: `cannot handle non-remotable resolution at syscall ${i}`, + }), + ); + } + syscalls = makeResult().syscalls; + pendingKref = /** @type {string} */ (settlement.slots[0]); + krefHistory.push(pendingKref); + continue nextKref; + } else { + if (!useRecord) useRecord = settlement; + syscalls = makeResult().syscalls; + pendingKref = /** @type {string} */ (settlement.kpid); + krefHistory.push(pendingKref); + continue nextKref; + } + } + } + syscalls = [...moreSyscalls, ...syscalls]; + } + return harden({ failure: `trace truncated`, krefHistory, syscalls }); +}; + +const startContractShape = M.splitRecord({ + sourceVatID: ZOE_VAT_ID, + targetKref: VAT_ADMIN_SERVICE_KREF, + methargs: makeMethargsShape('createVat', [ + M.remotable('zcfBundleCap'), + { + name: M.string(), + vatParameters: M.splitRecord({ contractBundleCap: M.remotable() }), + }, + ]), +}); + +/** `getPayouts()` and `numWantsSatisfied()` methargs */ +const methargsZoeSeatMethodShape = harden([ + M.or('getPayouts', 'numWantsSatisfied'), + [], +]); + +/** + * Searches for and decodes the syscall.send whose result corresponds with a provided kpid, + * or explains failure to do so. + * + * @param {import('better-sqlite3').Statement<[kpid: string]>} getSyscallsForKref + * @param {string} kpid + * @returns {{failure: string, kpidSyscalls: unknown[]} | ReturnType} + */ +const getSendForResultKpid = (getSyscallsForKref, kpid) => { + const kpidSyscalls = harden(getSyscallsForKref.all(kpid)); + const nonVatstoreSyscalls = harden( + kpidSyscalls.filter(({ type }) => !type.startsWith('vatstore')), + ); + if (!matches(nonVatstoreSyscalls, sendAndSubscribeShape)) { + return { failure: 'not send+subscribe', kpidSyscalls }; + } + const sendSyscall = decodeSendSyscall(kpidSyscalls[0]); + if (sendSyscall.resultKpid !== kpid) { + return { failure: 'not a send result kpid', kpidSyscalls }; + } + const subscribeSyscall = decodeSubscribeSyscall(nonVatstoreSyscalls[1]); + if (subscribeSyscall.kpid !== kpid) { + return { failure: 'missing subscription', kpidSyscalls }; + } + return sendSyscall; +}; + +const main = rawArgv => { + const { _: args, ...options } = yargsParser(rawArgv.slice(2)); + if (Reflect.ownKeys(options).length > 0 || args.length !== 1) { + const envVars = Object.entries({ + WALLET_FACTORY_VAT_ID, + ZOE_VAT_ID, + VAT_ADMIN_SERVICE_KREF, + ZOE_SERVICE_KREF, + }); + const q = str => `'${str.replaceAll("'", String.raw`'\''`)}'`; + console.error( + [ + `Usage: ${rawArgv[1]} /path/to/mezzanine-db.sqlite`, + '', + 'ENVIRONMENT', + envVars.map(([name, value]) => ` ${name}=${q(value)}`).join('\n'), + ].join('\n'), + ); + process.exitCode = EX_USAGE; + return; + } + + const [dbPath] = args; + const db = sqlite3(/** @type {string} */ (dbPath)); + const getUnsettledPromises = db.prepare(` + SELECT d.kpid, d.decider, s.subscriber + FROM promise_decider AS d + INNER JOIN promise_subscriber AS s USING (kpid) + ORDER BY 0+substring(d.kpid, 3) + `); + const getSyscallsForKref = db.prepare(` + SELECT s.* + FROM syscall_mention AS m + INNER JOIN syscall AS s USING (crankNum, syscallNum) + WHERE m.kref = ? + ORDER BY crankNum, syscallNum + `); + const _getDeliveriesForKref = db.prepare(` + SELECT d.* + FROM delivery_mention AS m + INNER JOIN delivery AS d USING (crankNum) + WHERE m.kref = ? + ORDER BY crankNum + `); + + nextPromise: for (const { + kpid, + decider, + subscriber, + } of getUnsettledPromises.all()) { + /** + * Reports the classification of this kpid. + * + * @param {string} type + * @param {unknown} details + */ + const classify = (type, details = undefined) => { + // Make `details` JSON-representable without affecting shallow object key order. + if (typeof details === 'object' && details !== null) { + details = Object.fromEntries( + JSON.parse(toCapData(harden(Object.entries(details))).body.slice(1)), + ); + } else if (details !== undefined) { + details = JSON.parse(toCapData(harden(details)).body.slice(1)); + } + console.log(JSON.stringify({ kpid, decider, subscriber, type, details })); + }; + + const sendData = getSendForResultKpid(getSyscallsForKref, kpid); + // @ts-expect-error destructuring of union type + const { kpidSyscalls, targetKref, methargs, resultKpid } = sendData; + // @ts-expect-error destructuring of union type + const { failure: generalFailure } = sendData; + if (generalFailure) { + classify(`unknown - ${generalFailure}`, { kpidSyscalls }); + continue nextPromise; + } + // kpid corresponds with the results of a syscall.send. + + if (matches(methargs, makeMethargsShape('done', []))) { + // kpid identifies `E(target).done()` results. + // Let's see if target is the adminNode of a contract vat created by Zoe, and if so identify the contract. + // @ts-expect-error destructuring of union type + const { failure, krefHistory, syscalls, request, useRecord } = + /** @type {SyscallTraceSuccess} */ ( + traceSyscalls(getSyscallsForKref, targetKref) + ); + if ( + !failure && + matches(request, startContractShape) && + useRecord && + krefOf(useRecord.data.adminNode) === targetKref + ) { + const [_createVat, createVatArgs] = request.methargs; + const [ + _zcfBundleCapPresence, + { name: vatName, vatParameters: _vatParameters }, + ] = createVatArgs; + for (const [pattern, re] of Object.entries(VAT_NAME_PATTERNS)) { + const match = vatName && vatName.match(re); + if (match) { + classify(`Zoe E(contractInstanceAdminNode).done()`, { + pattern, + ...match.groups, + }); + continue nextPromise; + } + } + classify(`Zoe E(contractInstanceAdminNode).done()`, { vatName }); + continue nextPromise; + } + classify(`unknown E(...).done()`, { failure, krefHistory, syscalls }); + continue nextPromise; + } else if ( + matches( + methargs, + makeMethargsShape( + 'getUpdateSince', + M.or([], [M.bigint()], [M.number()]), + ), + ) + ) { + // kpid corresponds with the results of a `getUpdateSince()` call on targetKref. + // @ts-expect-error destructuring of union type + const { failure, krefHistory, syscalls, request, useRecord } = + /** @type {SyscallTraceSuccess} */ ( + traceSyscalls(getSyscallsForKref, targetKref) + ); + if ( + !failure && + matches( + request, + M.splitRecord({ + sourceVatID: WALLET_FACTORY_VAT_ID, + methargs: makeMethargsShape('getCurrentAmountNotifier'), + }), + ) && + matches(useRecord?.data, M.remotable()) && + useRecord?.data.iface() === 'Alleged: notifier' + ) { + // targetKref identifies a current amount notifier requested by the wallet factory. + classify( + `WalletFactory E(likelyPurse).getCurrentAmountNotifier(...) .getUpdateSince(...)`, + { likelyPurseKref: request.targetKref }, + ); + continue nextPromise; + } + classify('unknown E(...).getUpdateSince(...)', { + targetKref, + failure, + krefHistory, + syscalls, + }); + continue nextPromise; + } else if (matches(methargs, methargsZoeSeatMethodShape)) { + // kpid corresponds with the results of a method call on targetKref that looks like a Zoe seat method. + const { + // @ts-expect-error destructuring of union type + failure, + krefHistory: _krefHistory, + syscalls: _syscalls, + request, + } = /** @type {SyscallTraceSuccess} */ ( + traceSyscalls(getSyscallsForKref, targetKref) + ); + if ( + !failure && + matches( + request, + M.splitRecord({ + targetKref: ZOE_SERVICE_KREF, + methargs: makeMethargsShape('offer'), + }), + ) + ) { + // targetKref does indeed identify a Zoe seat. Onward and upward... + const [invitationPresence] = request.methargs[1]; + const providedInvitationKpid = krefOf(invitationPresence); + const trace = /** @type {SyscallTraceSuccess} */ ( + traceSyscalls(getSyscallsForKref, providedInvitationKpid) + ); + if ( + matches( + trace.request?.methargs, + makeMethargsShape('makeInvitation', [ + M.remotable(), + M.string(), + M.any(), + M.pattern(), + ]), + ) + ) { + const { + sourceVatID: invitationSourceVatID, + targetKref: invitationCreatorKref, + resultKpid: invitationKpid, + } = trace.request; + classify(`E(invitation)[${methargs[0]}](...)`, { + invitationSourceVatID, + invitationCreatorKref, + invitationKpid, + }); + continue nextPromise; + } + classify(`unknown E(invitation)[${methargs[0]}](...)`, { trace }); + continue nextPromise; + } + } + classify(`unknown send result E(...).${String(methargs[0])}(...)`, { + targetKref, + methargs, + resultKpid, + }); + continue nextPromise; + } +}; + +main(process.argv); diff --git a/packages/kmarshal/src/kmarshal.js b/packages/kmarshal/src/kmarshal.js index efe62a452cfd..55328d3c7537 100644 --- a/packages/kmarshal/src/kmarshal.js +++ b/packages/kmarshal/src/kmarshal.js @@ -92,6 +92,8 @@ const [makeStandinPromise, getStandinPromiseTag] = (() => { } } })(); +export { makeStandinPromise }; +harden(makeStandinPromise); /** * @param {string} kref