From a029c6ff488c83fa2bc9250a6038da456609db9e Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Sun, 5 May 2024 22:00:23 -0700 Subject: [PATCH 01/10] feat(asyncFlow): Stopgap E support --- packages/async-flow/src/replay-membrane.js | 128 ++++++++++++++++-- packages/async-flow/src/type-guards.js | 14 +- packages/async-flow/src/types.js | 6 + .../test/replay-membrane-eventual.test.js | 108 ++++++++++++++- 4 files changed, 233 insertions(+), 23 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 48f680ec2ae..0b94da3fe7d 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -8,8 +8,9 @@ import { makeConvertKit } from './convert.js'; /** * @import {PromiseKit} from '@endo/promise-kit' + * @import {Passable, PassableCap} from '@endo/pass-style' * @import {Zone} from '@agoric/base-zone'; - * @import {Vow, VowTools} from '@agoric/vow' + * @import {Vow, VowTools, VowKit} from '@agoric/vow' * @import {AsyncFlow} from '../src/async-flow.js' * @import {LogStore} from '../src/log-store.js'; * @import {Bijection} from '../src/bijection.js'; @@ -32,7 +33,7 @@ export const makeReplayMembrane = ( watchWake, panic, ) => { - const { when } = vowTools; + const { when, makeVowKit } = vowTools; const equate = makeEquate(bijection); @@ -214,12 +215,111 @@ export const makeReplayMembrane = ( // //////////////// Eventual Send //////////////////////////////////////////// + /** + * @param {PassableCap} hostTarget + * @param {string | undefined} optVerb + * @param {Passable[]} hostArgs + * @param {number} callIndex + * @param {VowKit} hostResultKit + * @param {Promise} guestReturnedP + * @returns {Outcome} + */ + const performSend = ( + hostTarget, + optVerb, + hostArgs, + callIndex, + hostResultKit, + guestReturnedP, + ) => { + const { vow, resolver } = hostResultKit; + try { + const hostPromise = optVerb + ? E(hostTarget)[optVerb](...hostArgs) + : E(hostTarget)(...hostArgs); + resolver.resolve(hostPromise); // TODO does this always work? + } catch (hostProblem) { + throw Fail`internal: eventual send synchrously failed ${hostProblem}`; + } + try { + const entry = harden(['doReturn', callIndex, vow]); + log.pushEntry(entry); + const guestPromise = makeGuestForHostVow(vow, guestReturnedP); + // Note that `guestPromise` is not registered in the bijection since + // guestReturnedP is already the guest for vow. Rather, the handler + // returns guestPromise to resolve guestReturnedP to guestPromise. + const { kind } = doReturn(callIndex, vow); + kind === 'return' || Fail`internal: "return" kind expected ${q(kind)}`; + return harden({ + kind: 'return', + result: guestPromise, + }); + } catch (problem) { + throw panic(problem); + } + }; + const guestHandler = harden({ applyMethod(guestTarget, optVerb, guestArgs, guestReturnedP) { - if (optVerb === undefined) { - throw Panic`guest eventual call not yet supported: ${guestTarget}(${b(guestArgs)}) -> ${b(guestReturnedP)}`; - } else { - throw Panic`guest eventual send not yet supported: ${guestTarget}.${b(optVerb)}(${b(guestArgs)}) -> ${b(guestReturnedP)}`; + const callIndex = log.getIndex(); + if (stopped || !bijection.hasGuest(guestTarget)) { + Fail`Sent from a previous run: ${guestTarget}`; + } + // TODO FIX BUG this is not quite right. When guestResultP is returned + // as the resolution of guestResultP, it create a visious cycle error. + const hostResultKit = makeVowKit(); + bijection.init(guestReturnedP, hostResultKit.vow); + /** @type {Outcome} */ + let outcome; + try { + const guestEntry = harden([ + 'checkSend', + guestTarget, + optVerb, + guestArgs, + callIndex, + ]); + if (log.isReplaying()) { + const entry = log.nextEntry(); + equate( + guestEntry, + entry, + `replay ${callIndex}: + ${q(guestEntry)} + vs ${q(entry)} + `, + ); + outcome = /** @type {Outcome} */ (nestInterpreter(callIndex)); + } else { + const entry = guestToHost(guestEntry); + log.pushEntry(entry); + const [_op, hostTarget, _optVerb, hostArgs, _callIndex] = entry; + nestInterpreter(callIndex); + outcome = performSend( + hostTarget, + optVerb, + hostArgs, + callIndex, + hostResultKit, + guestReturnedP, + ); + } + } catch (fatalError) { + throw panic(fatalError); + } + + switch (outcome.kind) { + case 'return': { + return outcome.result; + } + case 'throw': { + throw outcome.problem; + } + default: { + // @ts-expect-error TS correctly knows this case would be outside + // the type. But that's what we want to check. + throw Panic`unexpected outcome kind ${q(outcome.kind)}`; + } } }, applyFunction(guestTarget, guestArgs, guestReturnedP) { @@ -321,11 +421,19 @@ export const makeReplayMembrane = ( /** * @param {Vow} hVow + * @param {Promise} [promiseKey] + * If provided, use this promise as the key in the guestPromiseMap + * rather than the returned promise. This only happens when the + * promiseKey ends up forwarded to the returned promise anyway, so + * associating it with this resolve/reject pair is not incorrect. + * It is needed when `promiseKey` is also entered into the bijection + * paired with hVow. * @returns {Promise} */ - const makeGuestForHostVow = hVow => { + const makeGuestForHostVow = (hVow, promiseKey = undefined) => { const { promise, resolve, reject } = makeGuestPromiseKit(); - guestPromiseMap.set(promise, harden({ resolve, reject })); + promiseKey ??= promise; + guestPromiseMap.set(promiseKey, harden({ resolve, reject })); watchWake(hVow); @@ -349,7 +457,7 @@ export const makeReplayMembrane = ( hVow, async hostFulfillment => { await log.promiseReplayDone(); // should never reject - if (!stopped && guestPromiseMap.get(promise) !== 'settled') { + if (!stopped && guestPromiseMap.get(promiseKey) !== 'settled') { /** @type {LogEntry} */ const entry = harden(['doFulfill', hVow, hostFulfillment]); log.pushEntry(entry); @@ -364,7 +472,7 @@ export const makeReplayMembrane = ( }, async hostReason => { await log.promiseReplayDone(); // should never reject - if (!stopped && guestPromiseMap.get(promise) !== 'settled') { + if (!stopped && guestPromiseMap.get(promiseKey) !== 'settled') { /** @type {LogEntry} */ const entry = harden(['doReject', hVow, hostReason]); log.pushEntry(entry); diff --git a/packages/async-flow/src/type-guards.js b/packages/async-flow/src/type-guards.js index 65551bd5f89..c3ebc526739 100644 --- a/packages/async-flow/src/type-guards.js +++ b/packages/async-flow/src/type-guards.js @@ -42,13 +42,13 @@ export const LogEntryShape = M.or( M.arrayOf(M.any()), M.number(), ], - // [ - // 'checkSend', - // M.or(M.remotable('host target'), VowShape), - // M.opt(PropertyKeyShape), - // M.arrayOf(M.any()), - // M.number(), - // ], + [ + 'checkSend', + M.or(M.remotable('host target'), VowShape), + M.opt(PropertyKeyShape), + M.arrayOf(M.any()), + M.number(), + ], // ['checkReturn', M.number(), M.any()], // ['checkThrow', M.number(), M.any()], ); diff --git a/packages/async-flow/src/types.js b/packages/async-flow/src/types.js index abf737421f7..5056818c664 100644 --- a/packages/async-flow/src/types.js +++ b/packages/async-flow/src/types.js @@ -97,6 +97,12 @@ export {}; * optVerb: PropertyKey|undefined, * args: Host[], * callIndex: number + * ] | [ + * op: 'checkSend', + * target: Host, + * optVerb: PropertyKey|undefined, + * args: Host[], + * callIndex: number * ]} LogEntry */ diff --git a/packages/async-flow/test/replay-membrane-eventual.test.js b/packages/async-flow/test/replay-membrane-eventual.test.js index fc6f771a470..2c39441e801 100644 --- a/packages/async-flow/test/replay-membrane-eventual.test.js +++ b/packages/async-flow/test/replay-membrane-eventual.test.js @@ -7,6 +7,7 @@ import { } from './prepare-test-env-ava.js'; import { Fail } from '@endo/errors'; +import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { prepareVowTools } from '@agoric/vow'; import { E } from '@endo/eventual-send'; // import E from '@agoric/vow/src/E.js'; @@ -46,15 +47,19 @@ const preparePingee = zone => */ const testFirstPlay = async (t, zone) => { const vowTools = prepareVowTools(zone); + const { makeVowKit } = vowTools; const makeLogStore = prepareLogStore(zone); const makeBijection = prepareBijection(zone); const makePingee = preparePingee(zone); + const { vow: v1, resolver: r1 } = zone.makeOnce('v1', () => makeVowKit()); + const { vow: _v2, resolver: _r2 } = zone.makeOnce('v2', () => makeVowKit()); const log = zone.makeOnce('log', () => makeLogStore()); const bij = zone.makeOnce('bij', makeBijection); const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic); + const p1 = mem.hostToGuest(v1); t.deepEqual(log.dump(), []); /** @type {Pingee} */ @@ -63,18 +68,105 @@ const testFirstPlay = async (t, zone) => { const guestPingee = mem.hostToGuest(pingee); t.deepEqual(log.dump(), []); - const pingTestSendResult = t.throwsAsync(() => E(guestPingee).ping('send'), { - message: - 'panic over "[Error: guest eventual send not yet supported: \\"[Alleged: Pingee guest wrapper]\\".ping([\\"send\\"]) -> \\"[Promise]\\"]"', - }); + const p = E(guestPingee).ping('send'); + + guestPingee.ping('call'); + + t.is(await p, undefined); + const dump = log.dump(); + const v3 = dump[3][2]; + t.deepEqual(dump, [ + ['checkCall', pingee, 'ping', ['call'], 0], + ['doReturn', 0, undefined], + ['checkSend', pingee, 'ping', ['send'], 2], + ['doReturn', 2, v3], + ['doFulfill', v3, undefined], + ]); + + r1.resolve('x'); + t.is(await p1, 'x'); + + t.deepEqual(log.dump(), [ + ['checkCall', pingee, 'ping', ['call'], 0], + ['doReturn', 0, undefined], + ['checkSend', pingee, 'ping', ['send'], 2], + ['doReturn', 2, v3], + ['doFulfill', v3, undefined], + ['doFulfill', v1, 'x'], + ]); +}; + +/** + * @param {any} t + * @param {Zone} zone + */ +const testReplay = async (t, zone) => { + const vowTools = prepareVowTools(zone); + prepareLogStore(zone); + prepareBijection(zone); + preparePingee(zone); + const { vow: v1 } = zone.makeOnce('v1', () => Fail`need v1`); + const { vow: v2, resolver: r2 } = zone.makeOnce('v2', () => Fail`need v2`); + + const log = /** @type {LogStore} */ ( + zone.makeOnce('log', () => Fail`need log`) + ); + const bij = /** @type {Bijection} */ ( + zone.makeOnce('bij', () => Fail`need bij`) + ); + + const pingee = zone.makeOnce('pingee', () => Fail`need pingee`); + + const dump = log.dump(); + const v3 = dump[3][2]; + t.deepEqual(dump, [ + ['checkCall', pingee, 'ping', ['call'], 0], + ['doReturn', 0, undefined], + ['checkSend', pingee, 'ping', ['send'], 2], + ['doReturn', 2, v3], + ['doFulfill', v3, undefined], + ['doFulfill', v1, 'x'], + ]); + + const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic); + t.true(log.isReplaying()); + t.is(log.getIndex(), 0); + + const guestPingee = mem.hostToGuest(pingee); + const p2 = mem.hostToGuest(v2); + // @ts-expect-error TS doesn't know that r2 is a resolver + r2.resolve('y'); + await eventLoopIteration(); + + const p1 = mem.hostToGuest(v1); + mem.wake(); + t.true(log.isReplaying()); + t.is(log.getIndex(), 0); + t.deepEqual(log.dump(), [ + ['checkCall', pingee, 'ping', ['call'], 0], + ['doReturn', 0, undefined], + ['checkSend', pingee, 'ping', ['send'], 2], + ['doReturn', 2, v3], + ['doFulfill', v3, undefined], + ['doFulfill', v1, 'x'], + ]); + + E(guestPingee).ping('send'); guestPingee.ping('call'); - await pingTestSendResult; + t.is(await p1, 'x'); + t.is(await p2, 'y'); + t.false(log.isReplaying()); t.deepEqual(log.dump(), [ ['checkCall', pingee, 'ping', ['call'], 0], ['doReturn', 0, undefined], + ['checkSend', pingee, 'ping', ['send'], 2], + ['doReturn', 2, v3], + ['doFulfill', v3, undefined], + ['doFulfill', v1, 'x'], + ['doFulfill', v2, 'y'], ]); }; @@ -94,5 +186,9 @@ test.serial('test durable replay-membrane settlement', async t => { nextLife(); const zone1 = makeDurableZone(getBaggage(), 'durableRoot'); - return testFirstPlay(t, zone1); + await testFirstPlay(t, zone1); + + nextLife(); + const zone3 = makeDurableZone(getBaggage(), 'durableRoot'); + return testReplay(t, zone3); }); From b988aec01b54943422da78ddbded41e016f8ddd0 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 25 Jun 2024 12:31:08 -0700 Subject: [PATCH 02/10] fixup! merge repair --- packages/async-flow/src/replay-membrane.js | 4 +++- .../async-flow/test/replay-membrane-eventual.test.js | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index ea5eb853921..570b66bca1e 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -289,7 +289,9 @@ export const makeReplayMembrane = ({ // TODO FIX BUG this is not quite right. When guestResultP is returned // as the resolution of guestResultP, it create a visious cycle error. const hostResultKit = makeVowKit(); - bijection.init(guestReturnedP, hostResultKit.vow); + const g = bijection.unwrapInit(guestReturnedP, hostResultKit.vow); + g === guestReturnedP || + Fail`internal: guestReturnedP should not unwrap: ${g} vs ${guestReturnedP}`; /** @type {Outcome} */ let outcome; try { diff --git a/packages/async-flow/test/replay-membrane-eventual.test.js b/packages/async-flow/test/replay-membrane-eventual.test.js index 986350409bf..94d1f07f598 100644 --- a/packages/async-flow/test/replay-membrane-eventual.test.js +++ b/packages/async-flow/test/replay-membrane-eventual.test.js @@ -117,7 +117,7 @@ const testReplay = async (t, zone) => { const log = /** @type {LogStore} */ ( zone.makeOnce('log', () => Fail`need log`) ); - const bij = /** @type {Bijection} */ ( + const bijection = /** @type {Bijection} */ ( zone.makeOnce('bij', () => Fail`need bij`) ); @@ -134,7 +134,13 @@ const testReplay = async (t, zone) => { ['doFulfill', v1, 'x'], ]); - const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic); + const mem = makeReplayMembrane({ + log, + bijection, + vowTools, + watchWake, + panic, + }); t.true(log.isReplaying()); t.is(log.getIndex(), 0); From f5c4d8c848d329e2cc87c1512ac4555feb7f16e6 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 25 Jun 2024 12:39:18 -0700 Subject: [PATCH 03/10] fixup! minor --- packages/async-flow/src/replay-membrane.js | 2 -- packages/orchestration/test/examples/sendAnywhere.test.ts | 5 +---- packages/orchestration/test/examples/swapExample.test.ts | 5 +---- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 570b66bca1e..710121e03eb 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -11,9 +11,7 @@ import { makeConvertKit } from './convert.js'; /** * @import {PromiseKit} from '@endo/promise-kit' * @import {Passable, PassableCap} from '@endo/pass-style' - * @import {Zone} from '@agoric/base-zone'; * @import {Vow, VowTools, VowKit} from '@agoric/vow' - * @import {AsyncFlow} from '../src/async-flow.js' * @import {LogStore} from '../src/log-store.js'; * @import {Bijection} from '../src/bijection.js'; * @import {Host, HostVow, LogEntry, Outcome} from '../src/types.js'; diff --git a/packages/orchestration/test/examples/sendAnywhere.test.ts b/packages/orchestration/test/examples/sendAnywhere.test.ts index b552beffa1e..35e0f049404 100644 --- a/packages/orchestration/test/examples/sendAnywhere.test.ts +++ b/packages/orchestration/test/examples/sendAnywhere.test.ts @@ -51,10 +51,7 @@ test('single amount proposal shape (keyword record)', async t => { } }); -// Failing with "guest eventual send not yet supported:" -// in withdrawFromSeat, at -// `return E(tempUserSeatP).getPayouts();` -test.failing('send using arbitrary chain info', async t => { +test('send using arbitrary chain info', async t => { t.log('bootstrap, orchestration core-eval'); const { bootstrap, diff --git a/packages/orchestration/test/examples/swapExample.test.ts b/packages/orchestration/test/examples/swapExample.test.ts index fa502cfd0bb..c10c4b2a994 100644 --- a/packages/orchestration/test/examples/swapExample.test.ts +++ b/packages/orchestration/test/examples/swapExample.test.ts @@ -12,10 +12,7 @@ const contractFile = `${dirname}/../../src/examples/swapExample.contract.js`; type StartFn = typeof import('@agoric/orchestration/src/examples/swapExample.contract.js').start; -// Failing with "guest eventual send not yet supported:" -// in withdrawFromSeat, at -// `return E(tempUserSeatP).getPayouts();` -test.failing('start', async t => { +test('start', async t => { const { bootstrap, brands: { ist }, From b1fc76d2158331b9b30c186d2d586eefa54dc15e Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 25 Jun 2024 12:44:03 -0700 Subject: [PATCH 04/10] fixup! TODO done --- packages/async-flow/src/replay-membrane.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 710121e03eb..df093a8c860 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -284,8 +284,6 @@ export const makeReplayMembrane = ({ if (stopped || !bijection.hasGuest(guestTarget)) { Fail`Sent from a previous run: ${guestTarget}`; } - // TODO FIX BUG this is not quite right. When guestResultP is returned - // as the resolution of guestResultP, it create a visious cycle error. const hostResultKit = makeVowKit(); const g = bijection.unwrapInit(guestReturnedP, hostResultKit.vow); g === guestReturnedP || From 64d8b5c3882e524ec0dd55d7bed970f93beaca96 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 25 Jun 2024 21:52:09 -0700 Subject: [PATCH 05/10] fixup! host-side send should use heapVowE --- packages/async-flow/src/replay-membrane.js | 64 ++++++++++++++++------ 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index df093a8c860..48f85094c9d 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -1,16 +1,24 @@ /* eslint-disable no-use-before-define */ import { Fail, X, b, makeError, q } from '@endo/errors'; -import { isPromise } from '@endo/promise-kit'; -import { Far, Remotable, getInterfaceOf } from '@endo/pass-style'; +import { + Far, + Remotable, + getInterfaceOf, + getTag, + makeTagged, + passStyleOf, +} from '@endo/pass-style'; import { E } from '@endo/eventual-send'; +import { heapVowE } from '@agoric/vow/vat.js'; import { getMethodNames } from '@endo/eventual-send/utils.js'; +import { objectMap } from '@endo/common/object-map.js'; import { isVow } from '@agoric/vow/src/vow-utils.js'; import { makeEquate } from './equate.js'; import { makeConvertKit } from './convert.js'; /** * @import {PromiseKit} from '@endo/promise-kit' - * @import {Passable, PassableCap} from '@endo/pass-style' + * @import {Passable, PassableCap, CopyTagged} from '@endo/pass-style' * @import {Vow, VowTools, VowKit} from '@agoric/vow' * @import {LogStore} from '../src/log-store.js'; * @import {Bijection} from '../src/bijection.js'; @@ -129,17 +137,35 @@ export const makeReplayMembrane = ({ // ///////////// Guest to Host or consume log //////////////////////////////// const tolerateHostPromiseToVow = h => { - if (isPromise(h)) { - const e = Error('where warning happened'); - console.log('Warning for now: vow expected, not promise', h, e); - // TODO remove this stopgap. Here for now because host-side - // promises are everywhere! - // Note: A good place to set a breakpoint, or to uncomment the - // `debugger;` line, to work around bundling. - // debugger; - return watch(h); - } else { - return h; + const passStyle = passStyleOf(h); + switch (passStyle) { + case 'promise': { + const e = Error('where warning happened'); + console.log('Warning for now: vow expected, not promise', h, e); + // TODO remove this stopgap. Here for now because host-side + // promises are everywhere! + // Note: A good place to set a breakpoint, or to uncomment the + // `debugger;` line, to work around bundling. + // debugger; + return watch(h); + } + case 'copyRecord': { + return objectMap(h, tolerateHostPromiseToVow); + } + case 'copyArray': { + const a = /** @type {Array} */ (h); + return harden(a.map(tolerateHostPromiseToVow)); + } + case 'tagged': { + const t = /** @type {CopyTagged} */ (h); + if (isVow(t)) { + return h; + } + return makeTagged(getTag(t), tolerateHostPromiseToVow(t.payload)); + } + default: { + return h; + } } }; @@ -151,6 +177,7 @@ export const makeReplayMembrane = ({ : hostTarget(...hostArgs); // This is a temporary kludge anyway. But note that it only // catches the case where the promise is at the top of hostResult. + harden(hostResult); hostResult = tolerateHostPromiseToVow(hostResult); // Try converting here just to route the error correctly hostToGuest(hostResult, `converting ${optVerb || 'host'} result`); @@ -254,8 +281,13 @@ export const makeReplayMembrane = ({ const { vow, resolver } = hostResultKit; try { const hostPromise = optVerb - ? E(hostTarget)[optVerb](...hostArgs) - : E(hostTarget)(...hostArgs); + ? heapVowE(hostTarget)[optVerb](...hostArgs) + : // @ts-expect-error once we changed this from E to heapVowE, + // typescript started complaining that heapVowE(hostTarget) + // is not callable. I'm not sure if this is a just a typing bug + // in heapVowE or also reflects a runtime deficiency. But this + // case it not used yet anyway. + heapVowE(hostTarget)(...hostArgs); resolver.resolve(hostPromise); // TODO does this always work? } catch (hostProblem) { throw Fail`internal: eventual send synchrously failed ${hostProblem}`; From 96505b5d0cb9fe59182ad41066ba3234551a919a Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Tue, 25 Jun 2024 22:14:04 -0700 Subject: [PATCH 06/10] fixup! golden error --- packages/async-flow/src/replay-membrane.js | 7 +++++-- packages/async-flow/test/bad-host.test.js | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 48f85094c9d..35aaee862ea 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -282,11 +282,14 @@ export const makeReplayMembrane = ({ try { const hostPromise = optVerb ? heapVowE(hostTarget)[optVerb](...hostArgs) - : // @ts-expect-error once we changed this from E to heapVowE, + : // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error + // @ts-ignore once we changed this from E to heapVowE, // typescript started complaining that heapVowE(hostTarget) // is not callable. I'm not sure if this is a just a typing bug // in heapVowE or also reflects a runtime deficiency. But this - // case it not used yet anyway. + // case it not used yet anyway. We disable it + // with at-ts-ignore rather than at-ts-expect-error because + // the dependency-graph tests complains that the latter is unused. heapVowE(hostTarget)(...hostArgs); resolver.resolve(hostPromise); // TODO does this always work? } catch (hostProblem) { diff --git a/packages/async-flow/test/bad-host.test.js b/packages/async-flow/test/bad-host.test.js index a7d3bb8893c..d9e80944afd 100644 --- a/packages/async-flow/test/bad-host.test.js +++ b/packages/async-flow/test/bad-host.test.js @@ -141,7 +141,7 @@ const testBadHostReplay1 = async (t, zone) => { }, { message: - 'converting badMethod result: Remotables must be explicitly declared: "[Function nonPassableFunc]"', + 'Remotables must be explicitly declared: "[Function nonPassableFunc]"', }, ); t.log(' badHost replay1 guest error caused by host error', gErr); @@ -177,7 +177,7 @@ const testBadHostReplay1 = async (t, zone) => { 'doThrow', 2, Error( - 'converting badMethod result: Remotables must be explicitly declared: "[Function nonPassableFunc]"', + 'Remotables must be explicitly declared: "[Function nonPassableFunc]"', ), ], ['checkCall', badHost, 'badMethod', [], 4], From 28c2753dcc91b410d1e331df9bba13bc3bcfd0e7 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 26 Jun 2024 12:18:25 -0700 Subject: [PATCH 07/10] fixup! restore test.failing with different explanation --- .../orchestration/test/examples/swapExample.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/orchestration/test/examples/swapExample.test.ts b/packages/orchestration/test/examples/swapExample.test.ts index c10c4b2a994..62c4c7bbccb 100644 --- a/packages/orchestration/test/examples/swapExample.test.ts +++ b/packages/orchestration/test/examples/swapExample.test.ts @@ -12,7 +12,15 @@ const contractFile = `${dirname}/../../src/examples/swapExample.contract.js`; type StartFn = typeof import('@agoric/orchestration/src/examples/swapExample.contract.js').start; -test('start', async t => { +/* Not sure why it is failing. Possibly relevant symptoms. +``` +----- ComosOrchestrationAccountHolder.6 3 TODO: handle brand { brand: Object [Alleged: IST brand] {}, value: 10000000n } +REJECTED at top of event loop (Error#20) +Error#20: {"type":1,"data":"CmgKIy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRlEkEKGFVOUEFSU0FCTEVfQ0hBSU5fQUREUkVTUxISYWdvcmljMXZhbG9wZXJmdWZ1GhEKBXVmbGl4EggxMDAwMDAwMA==","memo":""} + at parseTxPacket (file:///Users/markmiller/src/ongithub/agoric/agoric-sdk/packages/orchestration/src/utils/packet.js:87:14) +``` +*/ +test.failing('start', async t => { const { bootstrap, brands: { ist }, From 358b8394dd7acfd9e7b015b08297f11a6a044883 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 26 Jun 2024 13:42:59 -0700 Subject: [PATCH 08/10] fixup! review suggestions --- packages/async-flow/src/replay-membrane.js | 37 ++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 35aaee862ea..3d93ae5ce90 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -9,6 +9,7 @@ import { passStyleOf, } from '@endo/pass-style'; import { E } from '@endo/eventual-send'; +import { throwLabeled } from '@endo/common/throw-labeled.js'; import { heapVowE } from '@agoric/vow/vat.js'; import { getMethodNames } from '@endo/eventual-send/utils.js'; import { objectMap } from '@endo/common/object-map.js'; @@ -136,6 +137,19 @@ export const makeReplayMembrane = ({ // ///////////// Guest to Host or consume log //////////////////////////////// + /** + * The host is not supposed to expose host-side promises to the membrane, + * since they cannot be stored durably or survive upgrade. We cannot just + * automatically wrap any such host promises with host vows, because that + * would mask upgrade hazards if an upgrade happens before the vow settles. + * However, during the transition, the current host APIs called by + * orchestration still return many promises. We want to generate diagnostics + * when we encounter them, but for now, automatically convert them to + * host vow anyway, just so integration testing can proceed to reveal + * additional problems beyond these. + * + * @param {Passable} h + */ const tolerateHostPromiseToVow = h => { const passStyle = passStyleOf(h); switch (passStyle) { @@ -150,7 +164,8 @@ export const makeReplayMembrane = ({ return watch(h); } case 'copyRecord': { - return objectMap(h, tolerateHostPromiseToVow); + const o = /** @type {object} */ (h); + return objectMap(o, tolerateHostPromiseToVow); } case 'copyArray': { const a = /** @type {Array} */ (h); @@ -293,7 +308,7 @@ export const makeReplayMembrane = ({ heapVowE(hostTarget)(...hostArgs); resolver.resolve(hostPromise); // TODO does this always work? } catch (hostProblem) { - throw Fail`internal: eventual send synchrously failed ${hostProblem}`; + throw Panic`internal: eventual send synchrously failed ${hostProblem}`; } try { const entry = harden(['doReturn', callIndex, vow]); @@ -302,8 +317,7 @@ export const makeReplayMembrane = ({ // Note that `guestPromise` is not registered in the bijection since // guestReturnedP is already the guest for vow. Rather, the handler // returns guestPromise to resolve guestReturnedP to guestPromise. - const { kind } = doReturn(callIndex, vow); - kind === 'return' || Fail`internal: "return" kind expected ${q(kind)}`; + doReturn(callIndex, vow); return harden({ kind: 'return', result: guestPromise, @@ -335,14 +349,19 @@ export const makeReplayMembrane = ({ ]); if (log.isReplaying()) { const entry = log.nextEntry(); - equate( - guestEntry, - entry, - `replay ${callIndex}: + try { + equate(guestEntry, entry); + } catch (equateErr) { + // TODO consider Richard Gibson's suggestion for a better way + // to keep track of the error labeling. + throwLabeled( + equateErr, + `replay ${callIndex}: ${q(guestEntry)} vs ${q(entry)} `, - ); + ); + } outcome = /** @type {Outcome} */ (nestInterpreter(callIndex)); } else { const entry = guestToHost(guestEntry); From 99ba180998e36893b7b79477317c95bf633052b6 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 26 Jun 2024 14:58:14 -0700 Subject: [PATCH 09/10] fixup! trying test.skip instead --- packages/orchestration/test/examples/swapExample.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/orchestration/test/examples/swapExample.test.ts b/packages/orchestration/test/examples/swapExample.test.ts index 62c4c7bbccb..703c0432156 100644 --- a/packages/orchestration/test/examples/swapExample.test.ts +++ b/packages/orchestration/test/examples/swapExample.test.ts @@ -20,7 +20,7 @@ Error#20: {"type":1,"data":"CmgKIy9jb3Ntb3Muc3Rha2luZy52MWJldGExLk1zZ0RlbGVnYXRl at parseTxPacket (file:///Users/markmiller/src/ongithub/agoric/agoric-sdk/packages/orchestration/src/utils/packet.js:87:14) ``` */ -test.failing('start', async t => { +test.skip('start', async t => { const { bootstrap, brands: { ist }, From bfdeaaac41140f331e6efce561e89948fa64f126 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 27 Jun 2024 12:02:24 -0700 Subject: [PATCH 10/10] fixup! review suggestions --- packages/async-flow/src/replay-membrane.js | 71 +++++++++++++++++++ packages/async-flow/src/type-guards.js | 14 ++++ packages/async-flow/src/types.js | 18 +++++ .../test/replay-membrane-eventual.test.js | 11 +++ 4 files changed, 114 insertions(+) diff --git a/packages/async-flow/src/replay-membrane.js b/packages/async-flow/src/replay-membrane.js index 3d93ae5ce90..5f513333fed 100644 --- a/packages/async-flow/src/replay-membrane.js +++ b/packages/async-flow/src/replay-membrane.js @@ -276,6 +276,29 @@ export const makeReplayMembrane = ({ // //////////////// Eventual Send //////////////////////////////////////////// + /** + * @param {PassableCap} hostTarget + * @param {string | undefined} optVerb + * @param {Passable[]} hostArgs + */ + const performSendOnly = (hostTarget, optVerb, hostArgs) => { + try { + optVerb + ? heapVowE.sendOnly(hostTarget)[optVerb](...hostArgs) + : // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error + // @ts-ignore once we changed this from E to heapVowE, + // typescript started complaining that heapVowE(hostTarget) + // is not callable. I'm not sure if this is a just a typing bug + // in heapVowE or also reflects a runtime deficiency. But this + // case it not used yet anyway. We disable it + // with at-ts-ignore rather than at-ts-expect-error because + // the dependency-graph tests complains that the latter is unused. + heapVowE.sendOnly(hostTarget)(...hostArgs); + } catch (hostProblem) { + throw Panic`internal: eventual sendOnly synchrously failed ${hostProblem}`; + } + }; + /** * @param {PassableCap} hostTarget * @param {string | undefined} optVerb @@ -328,6 +351,44 @@ export const makeReplayMembrane = ({ }; const guestHandler = harden({ + applyMethodSendOnly(guestTarget, optVerb, guestArgs) { + const callIndex = log.getIndex(); + if (stopped || !bijection.hasGuest(guestTarget)) { + Fail`Sent from a previous run: ${guestTarget}`; + } + try { + const guestEntry = harden([ + 'checkSendOnly', + guestTarget, + optVerb, + guestArgs, + callIndex, + ]); + if (log.isReplaying()) { + const entry = log.nextEntry(); + try { + equate(guestEntry, entry); + } catch (equateErr) { + // TODO consider Richard Gibson's suggestion for a better way + // to keep track of the error labeling. + throwLabeled( + equateErr, + `replay ${callIndex}: + ${q(guestEntry)} + vs ${q(entry)} + `, + ); + } + } else { + const entry = guestToHost(guestEntry); + log.pushEntry(entry); + const [_op, hostTarget, _optVerb, hostArgs, _callIndex] = entry; + performSendOnly(hostTarget, optVerb, hostArgs); + } + } catch (fatalError) { + throw panic(fatalError); + } + }, applyMethod(guestTarget, optVerb, guestArgs, guestReturnedP) { const callIndex = log.getIndex(); if (stopped || !bijection.hasGuest(guestTarget)) { @@ -395,6 +456,13 @@ export const makeReplayMembrane = ({ } } }, + applyFunctionSendOnly(guestTarget, guestArgs) { + return guestHandler.applyMethodSendOnly( + guestTarget, + undefined, + guestArgs, + ); + }, applyFunction(guestTarget, guestArgs, guestReturnedP) { return guestHandler.applyMethod( guestTarget, @@ -403,6 +471,9 @@ export const makeReplayMembrane = ({ guestReturnedP, ); }, + getSendOnly(guestTarget, prop) { + throw Panic`guest eventual getSendOnly not yet supported: ${guestTarget}.${b(prop)}`; + }, get(guestTarget, prop, guestReturnedP) { throw Panic`guest eventual get not yet supported: ${guestTarget}.${b(prop)} -> ${b(guestReturnedP)}`; }, diff --git a/packages/async-flow/src/type-guards.js b/packages/async-flow/src/type-guards.js index c3ebc526739..8c3b641235d 100644 --- a/packages/async-flow/src/type-guards.js +++ b/packages/async-flow/src/type-guards.js @@ -23,6 +23,13 @@ export const LogEntryShape = M.or( // M.number(), // ], // [ + // 'doSendOnly', + // M.or(M.remotable('host wrapper of guest target'), VowShape), + // M.opt(PropertyKeyShape), + // M.arrayOf(M.any()), + // M.number(), + // ], + // [ // 'doSend', // M.or(M.remotable('host wrapper of guest target'), VowShape), // M.opt(PropertyKeyShape), @@ -42,6 +49,13 @@ export const LogEntryShape = M.or( M.arrayOf(M.any()), M.number(), ], + [ + 'checkSendOnly', + M.or(M.remotable('host target'), VowShape), + M.opt(PropertyKeyShape), + M.arrayOf(M.any()), + M.number(), + ], [ 'checkSend', M.or(M.remotable('host target'), VowShape), diff --git a/packages/async-flow/src/types.js b/packages/async-flow/src/types.js index 9faec27e892..8ecaeece849 100644 --- a/packages/async-flow/src/types.js +++ b/packages/async-flow/src/types.js @@ -104,6 +104,12 @@ export {}; * args: Host[], * callIndex: number * ] | [ + * op: 'checkSendOnly', + * target: Host, + * optVerb: PropertyKey|undefined, + * args: Host[], + * callIndex: number + * ] | [ * op: 'checkSend', * target: Host, * optVerb: PropertyKey|undefined, @@ -133,6 +139,12 @@ export {}; * args: Host[], * callIndex: number * ] | [ + * op: 'doSendOnly', + * target: Host, + * optVerb: PropertyKey|undefined, + * args: Host[], + * callIndex: number + * ] | [ * op: 'doSend', * target: Host, * optVerb: PropertyKey|undefined, @@ -161,6 +173,12 @@ export {}; * args: Host[], * callIndex: number * ] | [ + * op: 'checkSendOnly', + * target: Host, + * optVerb: PropertyKey|undefined, + * args: Host[], + * callIndex: number + * ] | [ * op: 'checkSend', * target: Host, * optVerb: PropertyKey|undefined, diff --git a/packages/async-flow/test/replay-membrane-eventual.test.js b/packages/async-flow/test/replay-membrane-eventual.test.js index 94d1f07f598..460b8964348 100644 --- a/packages/async-flow/test/replay-membrane-eventual.test.js +++ b/packages/async-flow/test/replay-membrane-eventual.test.js @@ -75,6 +75,8 @@ const testFirstPlay = async (t, zone) => { t.deepEqual(log.dump(), []); const p = E(guestPingee).ping('send'); + const pOnly = E.sendOnly(guestPingee).ping('sendOnly'); + t.is(pOnly, undefined); guestPingee.ping('call'); @@ -86,6 +88,7 @@ const testFirstPlay = async (t, zone) => { ['doReturn', 0, undefined], ['checkSend', pingee, 'ping', ['send'], 2], ['doReturn', 2, v3], + ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], ]); @@ -97,6 +100,7 @@ const testFirstPlay = async (t, zone) => { ['doReturn', 0, undefined], ['checkSend', pingee, 'ping', ['send'], 2], ['doReturn', 2, v3], + ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], ['doFulfill', v1, 'x'], ]); @@ -130,6 +134,7 @@ const testReplay = async (t, zone) => { ['doReturn', 0, undefined], ['checkSend', pingee, 'ping', ['send'], 2], ['doReturn', 2, v3], + ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], ['doFulfill', v1, 'x'], ]); @@ -159,11 +164,16 @@ const testReplay = async (t, zone) => { ['doReturn', 0, undefined], ['checkSend', pingee, 'ping', ['send'], 2], ['doReturn', 2, v3], + ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], ['doFulfill', v1, 'x'], ]); E(guestPingee).ping('send'); + // TODO Once https://github.com/endojs/endo/issues/2336 is fixed, + // the following `void` should not be needed. But strangely, TS isn't + // telling me a `void` is needed above, which is also incorrect. + void E.sendOnly(guestPingee).ping('sendOnly'); guestPingee.ping('call'); @@ -176,6 +186,7 @@ const testReplay = async (t, zone) => { ['doReturn', 0, undefined], ['checkSend', pingee, 'ping', ['send'], 2], ['doReturn', 2, v3], + ['checkSendOnly', pingee, 'ping', ['sendOnly'], 4], ['doFulfill', v3, undefined], ['doFulfill', v1, 'x'], ['doFulfill', v2, 'y'],