Skip to content

Commit

Permalink
feat(async-flow): error on guest E use (#9443)
Browse files Browse the repository at this point in the history
closes: #XXXX
refs: #9299 #9322

## Description

Prepare for #9322 by making any guest use of `E` until then cause an
error. We expect that it might be a while before #9322 is ready for
review. By merging this PR soon, we prevent any guest code or logs that
would commit us to an incompat way of handling `E`.

### Security Considerations

none
### Scaling Considerations

none
### Documentation Considerations

Should document that guests cannot invoke host vows or remotables with
`E` until #9322 , which won't happen immediately.
### Testing Considerations

- [x] need to test what kind of error state this goes into. Should be a
panic, so that an upgrade can unblock guest execution that got stuck
trying to `E`.
### Upgrade Considerations

The point. By making such use of `E` an error now, we ensure that #9322
can proceed without causing any compat problem with committed durable
state.

Co-authored-by: Mathieu Hofman <86499+mhofman@users.noreply.github.com>
  • Loading branch information
erights and mhofman authored Jun 10, 2024
1 parent 96c19f5 commit e193e66
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 10 deletions.
77 changes: 67 additions & 10 deletions packages/async-flow/src/replay-membrane.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/* eslint-disable no-use-before-define */
import { Fail, b, q } from '@endo/errors';
import { Fail, X, b, makeError, q } from '@endo/errors';
import { Far, Remotable, getInterfaceOf } from '@endo/pass-style';
import { E } from '@endo/eventual-send';
import { getMethodNames } from '@endo/eventual-send/utils.js';
import { makePromiseKit } from '@endo/promise-kit';
import { makeEquate } from './equate.js';
import { makeConvertKit } from './convert.js';

const { fromEntries, defineProperties } = Object;
/**
* @import {PromiseKit} from '@endo/promise-kit'
*/

const { fromEntries, defineProperties, assign } = Object;

/**
* @param {LogStore} log
Expand All @@ -31,6 +34,8 @@ export const makeReplayMembrane = (

let stopped = false;

const Panic = (template, ...args) => panic(makeError(X(template, ...args)));

// ////////////// Host or Interpreter to Guest ///////////////////////////////

/**
Expand Down Expand Up @@ -196,9 +201,61 @@ export const makeReplayMembrane = (
default: {
// @ts-expect-error TS correctly knows this case would be outside
// the type. But that's what we want to check.
throw Fail`unexpected outcome kind ${q(outcome.kind)}`;
throw Panic`unexpected outcome kind ${q(outcome.kind)}`;
}
}
};

// //////////////// Eventual Send ////////////////////////////////////////////

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)}`;
}
},
applyFunction(guestTarget, guestArgs, guestReturnedP) {
return guestHandler.applyMethod(
guestTarget,
undefined,
guestArgs,
guestReturnedP,
);
},
get(guestTarget, prop, guestReturnedP) {
throw Panic`guest eventual get not yet supported: ${guestTarget}.${b(prop)} -> ${b(guestReturnedP)}`;
},
});

const makeGuestPresence = (iface, methodEntries) => {
let guestPresence;
void new HandledPromise((_res, _rej, resolveWithPresence) => {
guestPresence = resolveWithPresence(guestHandler);
}); // no unfulfilledHandler
if (typeof guestPresence !== 'object') {
throw Fail`presence expected to be object ${guestPresence}`;
}
assign(guestPresence, fromEntries(methodEntries));
const result = Remotable(iface, undefined, guestPresence);
result === guestPresence ||
Fail`Remotable expected to make presence in place: ${guestPresence} vs ${result}`;
return result;
};

/**
* @returns {PromiseKit<any>}
*/
const makeGuestPromiseKit = () => {
let resolve;
let reject;
const promise = new HandledPromise((res, rej, _resPres) => {
resolve = res;
reject = rej;
}, guestHandler);
// @ts-expect-error TS cannot infer that it is a PromiseKit
return harden({ promise, resolve, reject });
};

// //////////////// Converters ///////////////////////////////////////////////
Expand Down Expand Up @@ -246,9 +303,7 @@ export const makeReplayMembrane = (
name,
makeGuestMethod(name),
]);
// TODO in order to support E *well*,
// use HandledPromise to make gRem a remote presence for hRem
gRem = Remotable(guestIface, undefined, fromEntries(guestMethods));
gRem = makeGuestPresence(guestIface, guestMethods);
}
// See note at the top of the function to see why clearing the `hRem`
// variable is safe, and what invariant the above code needs to maintain so
Expand All @@ -258,10 +313,12 @@ export const makeReplayMembrane = (
};
harden(makeGuestForHostRemotable);

/**
* @param {Vow} hVow
* @returns {Promise}
*/
const makeGuestForHostVow = hVow => {
// TODO in order to support E *well*,
// use HandledPromise to make `promise` a handled promise for hVow
const { promise, resolve, reject } = makePromiseKit();
const { promise, resolve, reject } = makeGuestPromiseKit();
guestPromiseMap.set(promise, harden({ resolve, reject }));

watchWake(hVow);
Expand Down
91 changes: 91 additions & 0 deletions packages/async-flow/test/replay-membrane-eventual.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// eslint-disable-next-line import/order
import {
test,
getBaggage,
annihilate,
nextLife,
} from './prepare-test-env-ava.js';

import { Fail } from '@endo/errors';
import { prepareVowTools } from '@agoric/vow';
import { E } from '@endo/eventual-send';
// import E from '@agoric/vow/src/E.js';
import { makeHeapZone } from '@agoric/zone/heap.js';
import { makeVirtualZone } from '@agoric/zone/virtual.js';
import { makeDurableZone } from '@agoric/zone/durable.js';

import { prepareLogStore } from '../src/log-store.js';
import { prepareBijection } from '../src/bijection.js';
import { makeReplayMembrane } from '../src/replay-membrane.js';

const watchWake = _vowish => {};
const panic = problem => Fail`panic over ${problem}`;

/**
* @param {Zone} zone
*/
const preparePingee = zone =>
zone.exoClass('Pingee', undefined, () => ({}), {
ping(_str) {},
});

/**
* @typedef {ReturnType<ReturnType<preparePingee>>} Pingee
*/

/**
* @param {any} t
* @param {Zone} zone
*/
const testFirstPlay = async (t, zone) => {
const vowTools = prepareVowTools(zone);
const makeLogStore = prepareLogStore(zone);
const makeBijection = prepareBijection(zone);
const makePingee = preparePingee(zone);

const log = zone.makeOnce('log', () => makeLogStore());
const bij = zone.makeOnce('bij', makeBijection);

const mem = makeReplayMembrane(log, bij, vowTools, watchWake, panic);

t.deepEqual(log.dump(), []);

/** @type {Pingee} */
const pingee = zone.makeOnce('pingee', () => makePingee());
/** @type {Pingee} */
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]\\"]"',
});

guestPingee.ping('call');

await pingTestSendResult;

t.deepEqual(log.dump(), [
['checkCall', pingee, 'ping', ['call'], 0],
['doReturn', 0, undefined],
]);
};

test.serial('test heap replay-membrane settlement', async t => {
const zone = makeHeapZone('heapRoot');
return testFirstPlay(t, zone);
});

test.serial('test virtual replay-membrane settlement', async t => {
annihilate();
const zone = makeVirtualZone('virtualRoot');
return testFirstPlay(t, zone);
});

test.serial('test durable replay-membrane settlement', async t => {
annihilate();

nextLife();
const zone1 = makeDurableZone(getBaggage(), 'durableRoot');
return testFirstPlay(t, zone1);
});

0 comments on commit e193e66

Please sign in to comment.