Skip to content

Commit 1875030

Browse files
authored
test orch flow upgrades by bridge message deferral (#9755)
refs: #9303 ## Description Tests restarting an orchestration flow while an IBC message is pending response. #9303 will be closed when this and #9719 have landed. ### Security Considerations none ### Scaling Considerations none ### Documentation Considerations none ### Testing Considerations per se ### Upgrade Considerations helps, no change
2 parents 77fcd1a + 4545c95 commit 1875030

File tree

6 files changed

+247
-38
lines changed

6 files changed

+247
-38
lines changed

Diff for: packages/boot/test/bootstrapTests/orchestration.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ test.serial('stakeAtom - smart wallet', async t => {
136136
buildProposal,
137137
evalProposal,
138138
agoricNamesRemotes,
139-
flushInboundQueue,
139+
bridgeUtils: { flushInboundQueue },
140140
readLatest,
141141
} = t.context;
142142

@@ -260,7 +260,7 @@ test('basic-flows', async t => {
260260
evalProposal,
261261
agoricNamesRemotes,
262262
readLatest,
263-
flushInboundQueue,
263+
bridgeUtils: { flushInboundQueue },
264264
} = t.context;
265265

266266
await evalProposal(
@@ -341,7 +341,7 @@ test.serial('basic-flows - portfolio holder', async t => {
341341
evalProposal,
342342
readLatest,
343343
agoricNamesRemotes,
344-
flushInboundQueue,
344+
bridgeUtils: { flushInboundQueue },
345345
} = t.context;
346346

347347
await evalProposal(

Diff for: packages/boot/test/bootstrapTests/vtransfer.test.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ test.before(async t => (t.context = await makeDefaultTestContext(t)));
2424
test.after.always(t => t.context.shutdown?.());
2525

2626
test('vtransfer', async t => {
27-
const { buildProposal, evalProposal, getOutboundMessages, runUtils } =
28-
t.context;
27+
const {
28+
buildProposal,
29+
evalProposal,
30+
bridgeUtils: { getOutboundMessages },
31+
runUtils,
32+
} = t.context;
2933
const { EV } = runUtils;
3034

3135
// Pull what transfer-proposal produced into local scope

Diff for: packages/boot/test/orchestration/restart-contracts.test.ts

+106-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ test.serial('sendAnywhere', async t => {
2424
walletFactoryDriver,
2525
buildProposal,
2626
evalProposal,
27-
flushInboundQueue,
27+
bridgeUtils: { flushInboundQueue },
2828
} = t.context;
2929

3030
const { IST } = t.context.agoricNamesRemotes.brand;
@@ -114,7 +114,7 @@ test('stakeAtom', async t => {
114114
buildProposal,
115115
evalProposal,
116116
agoricNamesRemotes,
117-
flushInboundQueue,
117+
bridgeUtils: { flushInboundQueue },
118118
readLatest,
119119
} = t.context;
120120

@@ -167,3 +167,107 @@ test('stakeAtom', async t => {
167167
t.is(await flushInboundQueue(), 1);
168168
t.true(hasResult(wd.getLatestUpdateRecord()));
169169
});
170+
171+
// Tests restart of an orchestration() flow while an IBC response is pending.
172+
//
173+
// TODO consider testing this pausing during any pending IBC message. It'll need
174+
// to fresh contract state on each iteration, and since this is a bootstrap test
175+
// that means either restarting bootstrap or starting a new contract and
176+
// restarting that one. For them to share bootstrap they'll each need a unique
177+
// instance name, which will require paramatizing the the two builders scripts
178+
// and the two core-eval functions.
179+
test.serial('basicFlows', async t => {
180+
const {
181+
walletFactoryDriver,
182+
buildProposal,
183+
evalProposal,
184+
bridgeUtils: { getInboundQueueLength, flushInboundQueue },
185+
} = t.context;
186+
187+
t.log('start basicFlows');
188+
await evalProposal(
189+
buildProposal('@agoric/builders/scripts/orchestration/init-basic-flows.js'),
190+
);
191+
192+
t.log('making offer');
193+
const wallet = await walletFactoryDriver.provideSmartWallet('agoric1test');
194+
const id1 = 'make-orch-account';
195+
// send because it won't resolve
196+
await wallet.sendOffer({
197+
id: id1,
198+
invitationSpec: {
199+
source: 'agoricContract',
200+
instancePath: ['basicFlows'],
201+
callPipe: [['makeOrchAccountInvitation']],
202+
},
203+
proposal: {},
204+
offerArgs: {
205+
chainName: 'cosmoshub',
206+
},
207+
});
208+
// no errors and no result yet
209+
t.like(wallet.getLatestUpdateRecord(), {
210+
status: {
211+
id: id1,
212+
error: undefined,
213+
numWantsSatisfied: 1,
214+
payouts: {},
215+
result: undefined, // no property
216+
},
217+
});
218+
t.is(getInboundQueueLength(), 1);
219+
220+
const id2 = 'makePortfolio';
221+
await wallet.sendOffer({
222+
id: id2,
223+
invitationSpec: {
224+
source: 'agoricContract',
225+
instancePath: ['basicFlows'],
226+
callPipe: [['makePortfolioAccountInvitation']],
227+
},
228+
proposal: {},
229+
offerArgs: {
230+
chainNames: ['agoric', 'cosmoshub'],
231+
},
232+
});
233+
// no errors and no result yet
234+
t.like(wallet.getLatestUpdateRecord(), {
235+
status: {
236+
id: id2,
237+
error: undefined,
238+
numWantsSatisfied: 1,
239+
payouts: {},
240+
result: undefined, // no property
241+
},
242+
});
243+
t.is(getInboundQueueLength(), 2);
244+
245+
t.log('restart basicFlows');
246+
await evalProposal(
247+
buildProposal('@agoric/builders/scripts/testing/restart-basic-flows.js'),
248+
);
249+
250+
t.log('flush and verify results');
251+
const beforeFlush = wallet.getLatestUpdateRecord();
252+
t.like(beforeFlush, {
253+
status: {
254+
result: undefined,
255+
},
256+
});
257+
t.is(await flushInboundQueue(1), 1);
258+
t.like(wallet.getLatestUpdateRecord(), {
259+
status: {
260+
id: id1,
261+
error: undefined,
262+
result: 'UNPUBLISHED',
263+
},
264+
});
265+
t.is(await flushInboundQueue(1), 1);
266+
t.like(wallet.getLatestUpdateRecord(), {
267+
status: {
268+
id: id2,
269+
error: undefined,
270+
result: 'UNPUBLISHED',
271+
},
272+
});
273+
});

Diff for: packages/boot/tools/supports.ts

+29-31
Original file line numberDiff line numberDiff line change
@@ -334,11 +334,6 @@ export const makeSwingsetTestKit = async (
334334
};
335335
let ibcSequenceNonce = 0;
336336

337-
const addSequenceNonce = ({ packet }: IBCMethod<'sendPacket'>): IBCPacket => {
338-
ibcSequenceNonce += 1;
339-
return { ...packet, sequence: ibcSequenceNonce };
340-
};
341-
342337
/**
343338
* Adds the sequence so the bridge knows what response to connect it to.
344339
* Then queue it send it over the bridge over this returns.
@@ -358,14 +353,18 @@ export const makeSwingsetTestKit = async (
358353
};
359354

360355
const inboundQueue: [bridgeId: BridgeIdValue, arg1: unknown][] = [];
356+
/** Add a message that will be sent to the bridge by flushInboundQueue. */
357+
const pushInbound = (bridgeId: BridgeIdValue, arg1: unknown) => {
358+
inboundQueue.push([bridgeId, arg1]);
359+
};
361360
/**
362361
* Like ackImmediately but defers in the inbound receiverAck
363362
* until `bridgeQueue()` is awaited.
364363
*/
365364
const ackLater = (obj: IBCMethod<'sendPacket'>, ack: string) => {
366365
ibcSequenceNonce += 1;
367366
const msg = icaMocks.ackPacketEvent(obj, ibcSequenceNonce, ack);
368-
inboundQueue.push([BridgeId.DIBC, msg]);
367+
pushInbound(BridgeId.DIBC, msg);
369368
return msg.packet;
370369
};
371370

@@ -437,10 +436,7 @@ export const makeSwingsetTestKit = async (
437436
case 'IBC_METHOD':
438437
switch (obj.method) {
439438
case 'startChannelOpenInit':
440-
inboundQueue.push([
441-
BridgeId.DIBC,
442-
icaMocks.channelOpenAck(obj),
443-
]);
439+
pushInbound(BridgeId.DIBC, icaMocks.channelOpenAck(obj));
444440
return undefined;
445441
case 'sendPacket':
446442
switch (obj.packet.data) {
@@ -619,36 +615,38 @@ export const makeSwingsetTestKit = async (
619615

620616
const getCrankNumber = () => Number(kernelStorage.kvStore.get('crankNumber'));
621617

622-
const getOutboundMessages = (bridgeId: string) =>
623-
harden([...outboundMessages.get(bridgeId)]);
624-
625-
/**
626-
* @param {number} max the max number of messages to flush
627-
* @returns {Promise<number>} the number of messages flushed
628-
*/
629-
const flushInboundQueue = async (max: number = Number.POSITIVE_INFINITY) => {
630-
console.log('🚽');
631-
let i = 0;
632-
for (i = 0; i < max; i += 1) {
633-
const args = inboundQueue.shift();
634-
if (!args) break;
635-
636-
await runUtils.queueAndRun(() => inbound(...args), true);
637-
}
638-
console.log('🧻');
639-
return i;
618+
const bridgeUtils = {
619+
/** Immediately handle the inbound message */
620+
inbound: bridgeInbound,
621+
getOutboundMessages: (bridgeId: string) =>
622+
harden([...outboundMessages.get(bridgeId)]),
623+
getInboundQueueLength: () => inboundQueue.length,
624+
/**
625+
* @param {number} max the max number of messages to flush
626+
* @returns {Promise<number>} the number of messages flushed
627+
*/
628+
async flushInboundQueue(max: number = Number.POSITIVE_INFINITY) {
629+
console.log('🚽');
630+
let i = 0;
631+
for (i = 0; i < max; i += 1) {
632+
const args = inboundQueue.shift();
633+
if (!args) break;
634+
635+
await runUtils.queueAndRun(() => inbound(...args), true);
636+
}
637+
console.log('🧻');
638+
return i;
639+
},
640640
};
641641

642642
return {
643643
advanceTimeBy,
644644
advanceTimeTo,
645+
bridgeUtils,
645646
buildProposal,
646-
bridgeInbound,
647647
controller,
648-
flushInboundQueue,
649648
evalProposal,
650649
getCrankNumber,
651-
getOutboundMessages,
652650
jumpTimeTo,
653651
readLatest,
654652
runUtils,
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* @file This is for use in tests.
3+
* Unlike most builder scripts, this one includes the proposal exports as well.
4+
*/
5+
import {
6+
deeplyFulfilledObject,
7+
makeTracer,
8+
NonNullish,
9+
} from '@agoric/internal';
10+
import { E } from '@endo/far';
11+
12+
/// <reference types="@agoric/vats/src/core/types-ambient"/>
13+
14+
const trace = makeTracer('RestartBasicFlows', true);
15+
16+
/**
17+
* @import {start as StartFn} from '@agoric/orchestration/src/examples/basic-flows.contract.js';
18+
*/
19+
20+
/**
21+
* @param {BootstrapPowers} powers
22+
*/
23+
export const restartBasicFlows = async ({
24+
consume: {
25+
agoricNames,
26+
board,
27+
chainStorage,
28+
chainTimerService,
29+
cosmosInterchainService,
30+
localchain,
31+
32+
contractKits,
33+
},
34+
instance: instances,
35+
}) => {
36+
trace(restartBasicFlows.name);
37+
38+
// @ts-expect-error unknown instance
39+
const instance = await instances.consume.basicFlows;
40+
trace('instance', instance);
41+
/** @type {StartedInstanceKit<StartFn>} */
42+
const kit = /** @type {any} */ (await E(contractKits).get(instance));
43+
44+
const marshaller = await E(board).getReadonlyMarshaller();
45+
46+
const privateArgs = await deeplyFulfilledObject(
47+
harden({
48+
agoricNames,
49+
localchain,
50+
marshaller,
51+
orchestrationService: cosmosInterchainService,
52+
storageNode: E(NonNullish(await chainStorage)).makeChildNode(
53+
'basicFlows',
54+
),
55+
timerService: chainTimerService,
56+
}),
57+
);
58+
59+
await E(kit.adminFacet).restartContract(privateArgs);
60+
trace('done');
61+
};
62+
harden(restartBasicFlows);
63+
64+
export const getManifest = () => {
65+
return {
66+
manifest: {
67+
[restartBasicFlows.name]: {
68+
consume: {
69+
agoricNames: true,
70+
board: true,
71+
chainStorage: true,
72+
chainTimerService: true,
73+
cosmosInterchainService: true,
74+
localchain: true,
75+
76+
contractKits: true,
77+
},
78+
instance: {
79+
consume: { basicFlows: true },
80+
},
81+
},
82+
},
83+
};
84+
};
85+
86+
/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */
87+
export const defaultProposalBuilder = async () =>
88+
harden({
89+
// Somewhat unorthodox, source the exports from this builder module
90+
sourceSpec: '@agoric/builders/scripts/testing/restart-basic-flows.js',
91+
getManifestCall: [getManifest.name],
92+
});
93+
94+
export default async (homeP, endowments) => {
95+
// import dynamically so the module can work in CoreEval environment
96+
const dspModule = await import('@agoric/deploy-script-support');
97+
const { makeHelpers } = dspModule;
98+
const { writeCoreEval } = await makeHelpers(homeP, endowments);
99+
await writeCoreEval(restartBasicFlows.name, defaultProposalBuilder);
100+
};

Diff for: packages/orchestration/src/exos/local-orchestration-account.js

+3
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,9 @@ export const prepareLocalOrchestrationAccountKit = (
468468
this.facets.transferWatcher,
469469
{ opts, amount, destination },
470470
);
471+
// FIXME https://github.com/Agoric/agoric-sdk/issues/9783
472+
// don't resolve the vow until the transfer is confirmed on remote
473+
// and reject vow if the transfer fails
471474
return watch(transferV, this.facets.returnVoidWatcher);
472475
});
473476
},

0 commit comments

Comments
 (0)