diff --git a/packages/boot/test/orchestration/contract-upgrade.test.ts b/packages/boot/test/orchestration/contract-upgrade.test.ts new file mode 100644 index 00000000000..c8dcc03f3c9 --- /dev/null +++ b/packages/boot/test/orchestration/contract-upgrade.test.ts @@ -0,0 +1,87 @@ +/** @file Bootstrap test of restarting contracts using orchestration */ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { TestFn } from 'ava'; + +import { + makeWalletFactoryContext, + type WalletFactoryTestContext, +} from '../bootstrapTests/walletFactory.js'; + +const test: TestFn = anyTest; +test.before(async t => { + t.context = await makeWalletFactoryContext( + t, + '@agoric/vm-config/decentral-itest-orchestration-config.json', + ); +}); +test.after.always(t => t.context.shutdown?.()); + +/** + * This test core-evals a buggy installation of the sendAnywhere contract by + * giving it a faulty `agoricNames` service with a lookup() function which + * returns a promise that never resolves. + * + * Because the send-anywhere flow requires a lookup(), it waits forever. This + * gives us a point at which we can upgrade the vat with a working agoricNames + * and see that the flow continues from that point. (The lookup call is not made + * directly in a flow, but instead from a host API which uses the retriable + * helper. As such it tests both the idempotent retry mechanism of retriable on + * upgrades, and the ability to resume an async-flow for which a host vow + * settles after an upgrade.) + */ +test.failing('resume', async t => { + const { walletFactoryDriver, buildProposal, evalProposal, storage } = + t.context; + + const { IST } = t.context.agoricNamesRemotes.brand; + + t.log('start sendAnywhere'); + await evalProposal( + buildProposal( + '@agoric/builders/scripts/testing/start-buggy-sendAnywhere.js', + ), + ); + + t.log('making offer'); + const wallet = await walletFactoryDriver.provideSmartWallet('agoric1test'); + // no money in wallet to actually send + const zero = { brand: IST, value: 0n }; + // send because it won't resolve + await wallet.sendOffer({ + id: 'send-somewhere', + invitationSpec: { + source: 'agoricContract', + instancePath: ['sendAnywhere'], + callPipe: [['makeSendInvitation']], + }, + proposal: { + // @ts-expect-error XXX BoardRemote + give: { Send: zero }, + }, + offerArgs: { destAddr: 'cosmos1whatever', chainName: 'cosmoshub' }, + }); + + // XXX golden test + const getLogged = () => + JSON.parse(storage.data.get('published.sendAnywhere.log')!).values; + + // This log shows the flow started, but didn't get past the name lookup + t.deepEqual(getLogged(), ['sending {0} from cosmoshub to cosmos1whatever']); + + t.log('upgrade sendAnywhere with fix'); + await evalProposal( + buildProposal('@agoric/builders/scripts/testing/fix-buggy-sendAnywhere.js'), + ); + + // FIXME https://github.com/Agoric/agoric-sdk/issues/9303 + // This doesn't yet get past 'sending' + t.deepEqual(getLogged(), [ + 'sending {0} from cosmoshub to cosmos1whatever', + 'got info for denoms: ibc/toyatom, ibc/toyusdc, ubld, uist', + 'got info for chain: cosmoshub cosmoshub-4', + 'completed transfer to localAccount', + // But does not get to a complete transaction without mocking the IBC transfer acknowledgementPacket + // TODO file a ticket for providing that and also fixing it in restart-contracts's .failing test + // 'transfer complete, seat exited', + ]); +}); diff --git a/packages/builders/scripts/testing/fix-buggy-sendAnywhere.js b/packages/builders/scripts/testing/fix-buggy-sendAnywhere.js new file mode 100644 index 00000000000..fdb31f11d44 --- /dev/null +++ b/packages/builders/scripts/testing/fix-buggy-sendAnywhere.js @@ -0,0 +1,142 @@ +/** + * @file This is for use in tests in a3p-integration + * Unlike most builder scripts, this one includes the proposal exports as well. + */ +import { + deeplyFulfilledObject, + makeTracer, + NonNullish, +} from '@agoric/internal'; +import { E, Far } from '@endo/far'; + +/// +/** + * @import {Installation, Instance} from '@agoric/zoe/src/zoeService/utils.js'; + */ + +const trace = makeTracer('FixBuggySA', true); + +/** + * @import {start as StartFn} from '@agoric/orchestration/src/examples/send-anywhere.contract.js'; + */ + +/** + * @param {BootstrapPowers & { + * instance: { + * consume: { + * sendAnywhere: Instance; + * }; + * }; + * }} powers + * @param {...any} rest + */ +export const fixSendAnywhere = async ( + { + consume: { + agoricNames, + board, + chainStorage, + chainTimerService, + contractKits, + cosmosInterchainService, + localchain, + }, + instance: instances, + }, + { options: { sendAnywhereRef } }, +) => { + trace(fixSendAnywhere.name); + + const saInstance = await instances.consume.sendAnywhere; + trace('saInstance', saInstance); + const saKit = await E(contractKits).get(saInstance); + + const marshaller = await E(board).getReadonlyMarshaller(); + + // This apparently pointless wrapper is to maintain structural parity + // with the buggy core-eval's wrapper to make lookup() hang. + const agoricNamesResolves = Far('agoricNames that resolves', { + lookup: async (...args) => { + return E(agoricNames).lookup(...args); + }, + }); + + const privateArgs = await deeplyFulfilledObject( + harden({ + agoricNames: agoricNamesResolves, + localchain, + marshaller, + orchestrationService: cosmosInterchainService, + storageNode: E(NonNullish(await chainStorage)).makeChildNode( + 'sendAnywhere', + ), + timerService: chainTimerService, + }), + ); + + trace('upgrading...'); + await E(saKit.adminFacet).upgradeContract( + sendAnywhereRef.bundleID, + privateArgs, + ); + + trace('done'); +}; +harden(fixSendAnywhere); + +export const getManifestForValueVow = ({ restoreRef }, { sendAnywhereRef }) => { + console.log('sendAnywhereRef', sendAnywhereRef); + return { + manifest: { + [fixSendAnywhere.name]: { + consume: { + agoricNames: true, + board: true, + chainStorage: true, + chainTimerService: true, + cosmosInterchainService: true, + localchain: true, + + contractKits: true, + }, + installation: { + consume: { sendAnywhere: true }, + }, + instance: { + consume: { sendAnywhere: true }, + }, + }, + }, + installations: { + sendAnywhere: restoreRef(sendAnywhereRef), + }, + options: { + sendAnywhereRef, + }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + // Somewhat unorthodox, source the exports from this builder module + sourceSpec: '@agoric/builders/scripts/testing/fix-buggy-sendAnywhere.js', + getManifestCall: [ + 'getManifestForValueVow', + { + sendAnywhereRef: publishRef( + install( + '@agoric/orchestration/src/examples/send-anywhere.contract.js', + ), + ), + }, + ], + }); + +export default async (homeP, endowments) => { + // import dynamically so the module can work in CoreEval environment + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval(fixSendAnywhere.name, defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/testing/start-buggy-sendAnywhere.js b/packages/builders/scripts/testing/start-buggy-sendAnywhere.js new file mode 100644 index 00000000000..7ba83f8c950 --- /dev/null +++ b/packages/builders/scripts/testing/start-buggy-sendAnywhere.js @@ -0,0 +1,142 @@ +/** + * @file This is for use in tests in a3p-integration + * Unlike most builder scripts, this one includes the proposal exports as well. + */ +import { + deeplyFulfilledObject, + makeTracer, + NonNullish, +} from '@agoric/internal'; +import { E, Far } from '@endo/far'; + +/// +/** + * @import {Installation} from '@agoric/zoe/src/zoeService/utils.js'; + */ + +const trace = makeTracer('StartBuggySA', true); + +/** + * @import {start as StartFn} from '@agoric/orchestration/src/examples/send-anywhere.contract.js'; + */ + +/** + * @param {BootstrapPowers & { + * installation: { + * consume: { + * sendAnywhere: Installation; + * }; + * }; + * }} powers + */ +export const startSendAnywhere = async ({ + consume: { + agoricNames, + board, + chainStorage, + chainTimerService, + cosmosInterchainService, + localchain, + startUpgradable, + }, + installation: { + consume: { sendAnywhere }, + }, + instance: { + // @ts-expect-error unknown instance + produce: { sendAnywhere: produceInstance }, + }, +}) => { + trace(startSendAnywhere.name); + + const marshaller = await E(board).getReadonlyMarshaller(); + + const privateArgs = await deeplyFulfilledObject( + harden({ + agoricNames, + localchain, + marshaller, + orchestrationService: cosmosInterchainService, + storageNode: E(NonNullish(await chainStorage)).makeChildNode( + 'sendAnywhere', + ), + timerService: chainTimerService, + }), + ); + + /** @type {import('@agoric/vats').NameHub} */ + // @ts-expect-error intentional fake + const agoricNamesHangs = Far('agoricNames that hangs', { + lookup: async () => { + trace('agoricNames.lookup being called that will never resolve'); + // BUG: this never resolves + return new Promise(() => {}); + }, + }); + + const { instance } = await E(startUpgradable)({ + label: 'sendAnywhere', + installation: sendAnywhere, + privateArgs: { + ...privateArgs, + agoricNames: agoricNamesHangs, + }, + }); + produceInstance.resolve(instance); + trace('done'); +}; +harden(startSendAnywhere); + +export const getManifestForValueVow = ({ restoreRef }, { sendAnywhereRef }) => { + trace('sendAnywhereRef', sendAnywhereRef); + return { + manifest: { + [startSendAnywhere.name]: { + consume: { + agoricNames: true, + board: true, + chainStorage: true, + chainTimerService: true, + cosmosInterchainService: true, + localchain: true, + + startUpgradable: true, + }, + installation: { + consume: { sendAnywhere: true }, + }, + instance: { + produce: { sendAnywhere: true }, + }, + }, + }, + installations: { + sendAnywhere: restoreRef(sendAnywhereRef), + }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + // Somewhat unorthodox, source the exports from this builder module + sourceSpec: '@agoric/builders/scripts/testing/start-buggy-sendAnywhere.js', + getManifestCall: [ + 'getManifestForValueVow', + { + sendAnywhereRef: publishRef( + install( + '@agoric/orchestration/src/examples/send-anywhere.contract.js', + ), + ), + }, + ], + }); + +export default async (homeP, endowments) => { + // import dynamically so the module can work in CoreEval environment + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval(startSendAnywhere.name, defaultProposalBuilder); +}; diff --git a/packages/orchestration/src/examples/send-anywhere.contract.js b/packages/orchestration/src/examples/send-anywhere.contract.js index 4fc4690fa2e..f555817d7dd 100644 --- a/packages/orchestration/src/examples/send-anywhere.contract.js +++ b/packages/orchestration/src/examples/send-anywhere.contract.js @@ -1,12 +1,15 @@ import { makeSharedStateRecord } from '@agoric/async-flow'; + import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; -import { withOrchestration } from '../utils/start-helper.js'; -import * as flows from './send-anywhere.flows.js'; import { prepareChainHubAdmin } from '../exos/chain-hub-admin.js'; import { AnyNatAmountShape } from '../typeGuards.js'; +import { withOrchestration } from '../utils/start-helper.js'; +import * as flows from './send-anywhere.flows.js'; /** + * @import {Vow} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; * @import {OrchestrationPowers, OrchestrationTools} from '../utils/start-helper.js'; */ @@ -29,11 +32,11 @@ harden(SingleNatAmountRecord); * @param {Zone} zone * @param {OrchestrationTools} tools */ -const contract = async ( +export const contract = async ( zcf, privateArgs, zone, - { chainHub, orchestrateAll, zoeTools }, + { chainHub, orchestrateAll, vowTools, zoeTools }, ) => { const contractState = makeSharedStateRecord( /** @type {{ account: OrchestrationAccount | undefined }} */ { @@ -43,9 +46,15 @@ const contract = async ( const creatorFacet = prepareChainHubAdmin(zone, chainHub); + // UNTIL https://github.com/Agoric/agoric-sdk/issues/9066 + const logNode = E(privateArgs.storageNode).makeChildNode('log'); + /** @type {(msg: string) => Vow} */ + const log = msg => vowTools.watch(E(logNode).setValue(msg)); + // orchestrate uses the names on orchestrationFns to do a "prepare" of the associated behavior const orchFns = orchestrateAll(flows, { contractState, + log, zoeTools, }); @@ -68,6 +77,7 @@ const contract = async ( return { publicFacet, creatorFacet }; }; +harden(contract); export const start = withOrchestration(contract); harden(start); diff --git a/packages/orchestration/src/examples/send-anywhere.flows.js b/packages/orchestration/src/examples/send-anywhere.flows.js index a72d5db77fd..74ab64b5f03 100644 --- a/packages/orchestration/src/examples/send-anywhere.flows.js +++ b/packages/orchestration/src/examples/send-anywhere.flows.js @@ -3,7 +3,8 @@ import { makeError, q } from '@endo/errors'; import { M, mustMatch } from '@endo/patterns'; /** - * @import {GuestInterface} from '@agoric/async-flow'; + * @import {GuestInterface, GuestOf} from '@agoric/async-flow'; + * @import {Vow} from '@agoric/vow'; * @import {ZoeTools} from '../utils/zoe-tools.js'; * @import {Orchestrator, LocalAccountMethods, OrchestrationAccountI, OrchestrationFlow} from '../types.js'; */ @@ -19,12 +20,13 @@ const { entries } = Object; * @param {object} ctx * @param {{ localAccount?: OrchestrationAccountI & LocalAccountMethods }} ctx.contractState * @param {GuestInterface} ctx.zoeTools + * @param {GuestOf<(msg: string) => Vow>} ctx.log * @param {ZCFSeat} seat * @param {{ chainName: string; destAddr: string }} offerArgs */ export const sendIt = async ( orch, - { contractState, zoeTools: { localTransfer, withdrawToSeat } }, + { contractState, log, zoeTools: { localTransfer, withdrawToSeat } }, seat, offerArgs, ) => { @@ -33,25 +35,30 @@ export const sendIt = async ( // NOTE the proposal shape ensures that the `give` is a single asset const { give } = seat.getProposal(); const [[_kw, amt]] = entries(give); + void log(`sending {${amt.value}} from ${chainName} to ${destAddr}`); const agoric = await orch.getChain('agoric'); const assets = await agoric.getVBankAssetInfo(); + void log(`got info for denoms: ${assets.map(a => a.denom).join(', ')}`); const { denom } = NonNullish( assets.find(a => a.brand === amt.brand), `${amt.brand} not registered in vbank`, ); - const chain = await orch.getChain(chainName); + // FIXME racy if (!contractState.localAccount) { - const agoricChain = await orch.getChain('agoric'); - contractState.localAccount = await agoricChain.makeAccount(); + contractState.localAccount = await agoric.makeAccount(); } + const chain = await orch.getChain(chainName); const info = await chain.getChainInfo(); const { chainId } = info; assert(typeof chainId === 'string', 'bad chainId'); + void log(`got info for chain: ${chainName} ${chainId}`); await localTransfer(seat, contractState.localAccount, give); + void log(`completed transfer to localAccount`); + try { await contractState.localAccount.transfer( { @@ -61,13 +68,16 @@ export const sendIt = async ( }, { denom, value: amt.value }, ); + void log(`completed transfer to ${destAddr}`); } catch (e) { await withdrawToSeat(contractState.localAccount, seat, give); const errorMsg = `IBC Transfer failed ${q(e)}`; + void log(`ERROR: ${errorMsg}`); seat.exit(errorMsg); throw makeError(errorMsg); } seat.exit(); + void log(`transfer complete, seat exited`); }; harden(sendIt); diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md index 5f4dbdca9a3..6d091f7bb4f 100644 --- a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md +++ b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.md @@ -44,6 +44,8 @@ Generated by [AVA](https://avajs.dev). 0: { contractState_kindHandle: 'Alleged: kind', contractState_singleton: 'Alleged: contractState', + log_kindHandle: 'Alleged: kind', + log_singleton: 'Alleged: log', zoeTools: { localTransfer_kindHandle: 'Alleged: kind', localTransfer_singleton: 'Alleged: localTransfer', diff --git a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap index bab47226e8b..0149838132a 100644 Binary files a/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap and b/packages/orchestration/test/examples/snapshots/send-anywhere.test.ts.snap differ