From 77018cf5cbadfca41081ca8a155cd0470bd69b5d Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 16 Jul 2024 14:16:02 -0700 Subject: [PATCH] test: upgrade from buggy sendAnywhere --- .../orchestration/contract-upgrade.test.ts | 90 ++++++++++++ .../scripts/testing/fix-buggy-sendAnywhere.js | 134 ++++++++++++++++++ .../testing/start-buggy-sendAnywhere.js | 129 +++++++++++++++++ .../examples/buggy-sendAnywhere.contract.js | 106 ++++++++++++++ 4 files changed, 459 insertions(+) create mode 100644 packages/boot/test/orchestration/contract-upgrade.test.ts create mode 100644 packages/builders/scripts/testing/fix-buggy-sendAnywhere.js create mode 100644 packages/builders/scripts/testing/start-buggy-sendAnywhere.js create mode 100644 packages/orchestration/src/examples/buggy-sendAnywhere.contract.js 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 000000000000..d81e797337fb --- /dev/null +++ b/packages/boot/test/orchestration/contract-upgrade.test.ts @@ -0,0 +1,90 @@ +/** @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 { AmountMath } from '@agoric/ertp'; +import { + makeWalletFactoryContext, + type WalletFactoryTestContext, +} from '../bootstrapTests/walletFactory.ts'; + +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?.()); + +test('resume', async t => { + const { walletFactoryDriver, buildProposal, evalProposal } = 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: 'hot1destAddr', chainName: 'hot' }, + }); + + // TODO verify in vstorage that the offer hangs + + t.log('upgrade sendAnywhere with fix'); + await t.throwsAsync( + evalProposal( + buildProposal( + '@agoric/builders/scripts/testing/fix-buggy-sendAnywhere.js', + ), + ), + ); + // FIXME the upgrade to fix is failing on: + /* + ----- Orchestrator.4 2 making an Orchestrator +Logging sent error stack (Error#1) +Error#1: replay 2: + ["checkCall","[Alleged: findBrandInVBank guest wrapper]","apply",[["[Alleged: IST brand guest wrapper]"]],2] + vs ["checkCall","[Alleged: findBrandInVBank]","apply",[["[Alleged: IST brand]"]],2] + : [1]: internal h->g: "[Alleged: findBrandInVBank]" -> "[Alleged: findBrandInVBank guest wrapper]" vs "[Function unwrapped]" + ["checkCall","[Alleged: findBrandInVBank guest wrapper]","apply",[["[Alleged: IST brand guest wrapper]"]],2] + vs ["checkCall","[Alleged: findBrandInVBank]","apply",[["[Alleged: IST brand]"]],2] + : [1]: internal h->g: "[Alleged: findBrandInVBank]" -> "[Alleged: findBrandInVBank guest wrapper]" vs "[Function unwrapped]" + at makeError (file:///opt/agoric/agoric-sdk/node_modules/ses/src/error/assert.js:347:61) + at throwLabeled (.../common/throw-labeled.js:23:16) + at equate (.../async-flow/src/equate.js:23:1) + at guestCallsHost (.../async-flow/src/replay-membrane.js:183:1) + at In "apply" method of (findBrandInVBank) [as apply] (.../async-flow/src/replay-membrane.js:466:8) + at unwrapped (.../async-flow/src/endowments.js:98:41) + at sendIt (.../orchestration/src/examples/sendAnywhereFlows.js:39:20) + at eval (.../async-flow/src/async-flow.js:222:1) + at Object.restart (.../async-flow/src/async-flow.js:222:30) + at In "restart" method of (asyncFlow flow) [as restart] (.../exo/src/exo-tools.js:171:14) + at Object.wake (.../async-flow/src/async-flow.js:311:6) + at In "wake" method of (asyncFlow flow) [as wake] (.../exo/src/exo-tools.js:171:14) + at Object.wakeAll (.../async-flow/src/async-flow.js:474:6) + at In "wakeAll" method of (AdminAsyncFlow) [as wakeAll] (.../exo/src/exo-tools.js:171:14) + at eval (.../async-flow/src/async-flow.js:487:48) + */ + + // TODO confirm in vstorage that the started offer resolves +}); 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 000000000000..6665d8e7c0c5 --- /dev/null +++ b/packages/builders/scripts/testing/fix-buggy-sendAnywhere.js @@ -0,0 +1,134 @@ +/** + * @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 } from '@endo/far'; + +/// +/** + * @import {Installation, Instance} from '@agoric/zoe/src/zoeService/utils.js'; + */ + +const trace = makeTracer('StartBuggySA', true); + +/** + * @import {start as StartFn} from '@agoric/orchestration/src/examples/buggy-sendAnywhere.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(); + + const privateArgs = await deeplyFulfilledObject( + harden({ + agoricNames, + 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/sendAnywhere.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 000000000000..7749b0db87f3 --- /dev/null +++ b/packages/builders/scripts/testing/start-buggy-sendAnywhere.js @@ -0,0 +1,129 @@ +/** + * @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 } 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/buggy-sendAnywhere.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, + }), + ); + + const { instance } = await E(startUpgradable)({ + label: 'sendAnywhere', + installation: sendAnywhere, + privateArgs, + }); + produceInstance.resolve(instance); + trace('done'); +}; +harden(startSendAnywhere); + +export const getManifestForValueVow = ({ restoreRef }, { sendAnywhereRef }) => { + console.log('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/buggy-sendAnywhere.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/buggy-sendAnywhere.contract.js b/packages/orchestration/src/examples/buggy-sendAnywhere.contract.js new file mode 100644 index 000000000000..f47412226918 --- /dev/null +++ b/packages/orchestration/src/examples/buggy-sendAnywhere.contract.js @@ -0,0 +1,106 @@ +/** + * @file variation of sendAnywhere.contract.js that has a vow that never + * resolves, for use in testing replacing this with a working contract during + * in-flight offers + */ +import { makeStateRecord } from '@agoric/async-flow'; +import { AmountShape } from '@agoric/ertp'; +import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; +import { Fail } from '@endo/errors'; +import { M } from '@endo/patterns'; +import { withOrchestration } from '../utils/start-helper.js'; +import { orchestrationFns } from './sendAnywhereFlows.js'; + +/** + * @import {TimerService} from '@agoric/time'; + * @import {LocalChain} from '@agoric/vats/src/localchain.js'; + * @import {NameHub} from '@agoric/vats'; + * @import {Remote, Vow} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; + * @import {VBankAssetDetail} from '@agoric/vats/tools/board-utils.js'; + * @import {CosmosChainInfo, IBCConnectionInfo} from '../cosmos-api'; + * @import {CosmosInterchainService} from '../exos/cosmos-interchain-service.js'; + * @import {OrchestrationTools} from '../utils/start-helper.js'; + */ + +/** + * @typedef {{ + * localchain: Remote; + * orchestrationService: Remote; + * storageNode: Remote; + * timerService: Remote; + * agoricNames: Remote; + * }} OrchestrationPowers + */ + +export const SingleAmountRecord = M.and( + M.recordOf(M.string(), AmountShape, { + numPropertiesLimit: 1, + }), + M.not(harden({})), +); + +/** + * Orchestration contract to be wrapped by withOrchestration for Zoe + * + * @param {ZCF} zcf + * @param {OrchestrationPowers & { + * marshaller: Marshaller; + * }} privateArgs + * @param {Zone} zone + * @param {OrchestrationTools} tools + */ +const contract = async ( + zcf, + privateArgs, + zone, + { orchestrateAll, vowTools, zoeTools }, +) => { + const contractState = makeStateRecord( + /** @type {{ account: OrchestrationAccount | undefined }} */ { + account: undefined, + }, + ); + + /** @type {(brand: Brand) => Vow} */ + const findBrandInVBank = vowTools.retriable( + zone, + 'findBrandInVBank', + /** @param {Brand} brand */ + async brand => { + // BUG: this never resolves + const assets = await new Promise(() => {}); + const it = assets.find(a => a.brand === brand); + it || Fail`brand ${brand} not in agoricNames.vbankAsset`; + return it; + }, + ); + + // orchestrate uses the names on orchestrationFns to do a "prepare" of the associated behavior + const orchFns = orchestrateAll(orchestrationFns, { + zcf, + contractState, + localTransfer: zoeTools.localTransfer, + findBrandInVBank, + }); + + const publicFacet = zone.exo( + 'Send PF', + M.interface('Send PF', { + makeSendInvitation: M.callWhen().returns(InvitationShape), + }), + { + makeSendInvitation() { + return zcf.makeInvitation( + orchFns.sendIt, + 'send', + undefined, + M.splitRecord({ give: SingleAmountRecord }), + ); + }, + }, + ); + return { publicFacet }; +}; + +export const start = withOrchestration(contract);