Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(vats): Implement vtransfer app intercept in the bridge registry rather than in middleware #9469

Merged
merged 3 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions packages/boot/test/bootstrapTests/vtransfer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import type { TestFn } from 'ava';

import type { ScopedBridgeManager } from '@agoric/vats';
import type {
TransferMiddleware,
TransferVat,
} from '@agoric/vats/src/vat-transfer.js';
import type { TransferMiddleware } from '@agoric/vats/src/transfer.js';
import type { TransferVat } from '@agoric/vats/src/vat-transfer.js';
import { BridgeId, VTRANSFER_IBC_EVENT } from '@agoric/internal';
import { makeSwingsetTestKit } from '../../tools/supports.ts';

Expand Down
18 changes: 12 additions & 6 deletions packages/orchestration/test/supports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,19 @@ export const commonSetup = async t => {
);

const transferBridge = makeFakeTransferBridge(rootZone);
const { makeTransferMiddleware, makeBridgeTargetKit } = prepareTransferTools(
rootZone.subZone('transfer'),
prepareVowTools(rootZone.subZone('vows')),
);
const transferMiddleware = makeTransferMiddleware(
makeBridgeTargetKit(transferBridge, VTRANSFER_IBC_EVENT),
const { makeTransferMiddlewareKit, makeBridgeTargetKit } =
prepareTransferTools(
rootZone.subZone('transfer'),
prepareVowTools(rootZone.subZone('vows')),
);
const { finisher, interceptorFactory, transferMiddleware } =
makeTransferMiddlewareKit();
const bridgeTargetKit = makeBridgeTargetKit(
transferBridge,
VTRANSFER_IBC_EVENT,
interceptorFactory,
);
finisher.useRegistry(bridgeTargetKit.targetRegistry);

const localchainBridge = makeFakeLocalchainBridge(rootZone);
const localchain = prepareLocalChainTools(
Expand Down
131 changes: 102 additions & 29 deletions packages/vats/src/bridge-target.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { BridgeHandlerI } from './bridge.js';
* too restrictive to work out-of-the-box.
*/

const { Fail } = assert;
const { details: X, Fail } = assert;

/**
* @typedef {object} TargetApp an object representing the app that receives
Expand All @@ -32,6 +32,28 @@ export const TargetHostI = M.interface('TargetHost', {
sendDowncall: M.callWhen(M.any()).returns(M.any()),
});

/**
* @typedef {object} AppTransformer an object for replacing a TargetApp,
* generally with a wrapper that intercepts its inputs and/or results to
* ensure conformance with a protocol and/or interface (e.g., an active tap
* can encode results of the wrapped TargetApp into an acknowledgement
* message)
* @property {(
* app: ERef<TargetApp>,
* targetHost: ERef<TargetHost>,
* ...args: unknown[]
* ) => ERef<TargetApp>} wrapApp
*/
// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163
export const AppTransformerI = M.interface('AppTransformer', {
wrapApp: M.callWhen(
M.await(M.remotable('TargetAppI')),
M.await(M.remotable('TargetHostI')),
)
.rest(M.any())
.returns(M.remotable('TargetAppI')),
});

/**
* @typedef {object} TargetRegistration is an ExoClass of its own, and each
* instance is an attenuation of a BridgeTargetKit `targetRegistry` facet that
Expand All @@ -54,19 +76,23 @@ export const TargetRegistrationI = M.interface('TargetRegistration', {
* @property {(
* target: string,
* targetApp: ERef<TargetApp>,
* args?: unknown[],
* ) => Promise<TargetRegistration>} register
* @property {(target: string, targetApp: ERef<TargetApp>) => Promise<void>} reregister
* @property {(
* target: string,
* targetApp: ERef<TargetApp>,
* args?: unknown[],
* ) => Promise<void>} reregister
* @property {(target: string) => Promise<void>} unregister
*/
// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163
const TargetRegistryI = M.interface('TargetRegistry', {
register: M.callWhen(M.string(), M.await(M.remotable('TargetAppI'))).returns(
M.remotable('TargetRegistration'),
),
reregister: M.callWhen(
M.string(),
M.await(M.remotable('TargetAppI')),
).returns(),
register: M.callWhen(M.string(), M.await(M.remotable('TargetAppI')))
.optional(M.array())
.returns(M.remotable('TargetRegistration')),
reregister: M.callWhen(M.string(), M.await(M.remotable('TargetAppI')))
.optional(M.array())
.returns(),
unregister: M.callWhen(M.string()).returns(),
});

Expand All @@ -78,11 +104,13 @@ export const prepareTargetRegistration = zone =>
/**
* @param {string} target
* @param {TargetRegistry} registry
* @param {unknown[] | null} args
*/
(target, registry) => ({
(target, registry, args) => ({
target,
/** @type {TargetRegistry | null} */
registry,
args,
}),
{
/**
Expand All @@ -91,11 +119,15 @@ export const prepareTargetRegistration = zone =>
* @param {TargetApp} app new app to handle messages for the target
*/
async updateTargetApp(app) {
const { target, registry } = this.state;
const { target, registry, args } = this.state;
if (!registry) {
throw Fail`Registration for ${target} is already revoked`;
}
return E(registry).reregister(target, app);
return E(registry).reregister(
target,
app,
/** @type {unknown[]} */ (args),
);
},
/** Atomically delete the registration. */
async revoke() {
Expand All @@ -104,6 +136,7 @@ export const prepareTargetRegistration = zone =>
throw Fail`Registration for ${target} is already revoked`;
}
this.state.registry = null;
this.state.args = null;
return E(registry).unregister(target);
},
},
Expand All @@ -112,18 +145,19 @@ export const prepareTargetRegistration = zone =>
/**
* A BridgeTargetKit is associated with a ScopedBridgeManager (essentially a
* specific named channel on a bridge, cf. {@link ./bridge.js}) and a particular
* inbound event type. It consists of three facets:
* inbound event type and an optional app transformer. It consists of three
* facets:
*
* - `targetHost` has a `downcall` method for sending outbound messages via the
* bridge to the VM host.
* - `targetRegistry` has `register`, `reregister` and `unregister` methods to
* register/reregister/unregister an "app" with a "target" corresponding to an
* address on the targetHost. Each target may be associated with at most one
* app at any given time, and registration and unregistration each send a
* message to the targetHost of the state change (of type
* "BRIDGE_TARGET_REGISTER" and "BRIDGE_TARGET_UNREGISTER", respectively).
* `reregister` is a method that atomically redirects the target to a new
* app.
* register/reregister/unregister an "app" (or rather its transformation) with
* a "target" corresponding to an address on the targetHost. Each target may
* be associated with at most one app at any given time, and registration and
* unregistration each send a message to the targetHost of the state change
* (of type "BRIDGE_TARGET_REGISTER" and "BRIDGE_TARGET_UNREGISTER",
* respectively). `reregister` is a method that atomically redirects the
* target to a new (transformed) app.
* - `bridgeHandler` has a `fromBridge` method for receiving from the
* ScopedBridgeManager inbound messages of the associated event type and
* dispatching them to the app registered for their target.
Expand All @@ -143,10 +177,12 @@ export const prepareBridgeTargetKit = (zone, makeTargetRegistration) =>
* @template {import('@agoric/internal').BridgeIdValue} T
* @param {import('./types').ScopedBridgeManager<T>} manager
* @param {string} inboundEventType
* @param {AppTransformer} [appTransformer]
*/
(manager, inboundEventType) => ({
(manager, inboundEventType, appTransformer = undefined) => ({
manager,
inboundEventType,
appTransformer,
/** @type {MapStore<string, ERef<TargetApp>>} */
targetToApp: zone.detached().mapStore('targetToApp'),
}),
Expand Down Expand Up @@ -177,36 +213,73 @@ export const prepareBridgeTargetKit = (zone, makeTargetRegistration) =>
*
* @param {string} target
* @param {TargetApp} app
* @param {unknown[]} [args]
* @returns {Promise<TargetRegistration>} power to set or delete the
* registration
*/
async register(target, app) {
async register(target, app, args = []) {
const { targetHost } = this.facets;
const { targetToApp } = this.state;
const { appTransformer, targetToApp } = this.state;

// Because wrapping an app is async, we verify absence of an existing
// registration twice (once to avoid the unnecessary invocation and
// once inside `init`), but attempt to throw similar errors in both
// cases.
!targetToApp.has(target) || Fail`Target ${target} already registered`;
const wrappedApp = await (appTransformer
? E(appTransformer).wrapApp(app, targetHost, ...args)
: app);
try {
targetToApp.init(target, wrappedApp);
} catch (cause) {
throw assert.error(
X`Target ${target} already registered`,
undefined,
{ cause },
);
}

targetToApp.init(target, app);
await E(targetHost).sendDowncall({
type: 'BRIDGE_TARGET_REGISTER',
target,
});

return makeTargetRegistration(target, this.facets.targetRegistry);
return makeTargetRegistration(
target,
this.facets.targetRegistry,
args,
);
},
/**
* Update the app that handles messages for a target.
*
* @param {string} target
* @param {TargetApp} app
* @param {unknown[]} [args]
* @returns {Promise<void>}
*/
async reregister(target, app) {
const { targetToApp } = this.state;
async reregister(target, app, args = []) {
const { targetHost } = this.facets;
const { appTransformer, targetToApp } = this.state;

// Because wrapping an app is async, we verify absence of an existing
// registration twice (once to avoid the unnecessary invocation and
// once inside `set`), but attempt to throw similar errors in both
// cases.
targetToApp.has(target) ||
Fail`Target ${target} is already unregistered`;

targetToApp.set(target, app);
const wrappedApp = await (appTransformer
? E(appTransformer).wrapApp(app, targetHost, ...args)
: app);
try {
targetToApp.set(target, wrappedApp);
} catch (cause) {
throw assert.error(
X`Target ${target} is already unregistered`,
undefined,
{ cause },
);
}
},
/**
* Unregister the target, bypassing the attenuated `TargetRegistration`
Expand Down
64 changes: 41 additions & 23 deletions packages/vats/src/proposals/transfer-proposal.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export const setupTransferMiddleware = async (
bridgeManager: bridgeManagerP,
vtransferBridgeManager: vtransferBridgeManagerP,
},
produce: { transferMiddleware, transferVat, vtransferBridgeManager },
produce: {
transferMiddleware: produceTransferMiddleware,
transferVat: produceTransferVat,
vtransferBridgeManager: produceVtransferBridgeManager,
},
},
options,
) => {
Expand All @@ -46,39 +50,53 @@ export const setupTransferMiddleware = async (
};
// don't proceed if loadCriticalVat fails
await Promise.all(Object.values(vats));
produceTransferVat.reset();
produceTransferVat.resolve(vats.transfer);

transferVat.reset();
transferVat.resolve(vats.transfer);
const provideKit = mgr =>
E(vats.transfer).provideBridgeTargetKit(mgr, VTRANSFER_IBC_EVENT);
/** @type {Awaited<ReturnType<typeof provideKit>>} */
let kit;
// We'll be exporting a TransferMiddleware instance that implements a
// vtransfer app registry supporting active and passive "taps" while handling
// IBC transfer protocol conformance by configuring its backing
// BridgeTargetKit registry to wrap each app with an interceptor.
// But a bridge channel scoped to vtransfer might already exist, so we make or
// retrieve it as appropriate, then make its intercepting BridgeTargetKit,
// and then finally configure the TransferMiddleware with that kit's registry.
const { finisher, interceptorFactory, transferMiddleware } = await E(
vats.transfer,
).makeTransferMiddlewareKit();
const vtransferID = BRIDGE_ID.VTRANSFER;
const provideBridgeTargetKit = bridge =>
E(vats.transfer).provideBridgeTargetKit(
bridge,
VTRANSFER_IBC_EVENT,
interceptorFactory,
);
/** @type {Awaited<ReturnType<typeof provideBridgeTargetKit>>} */
let bridgeTargetKit;
try {
const m = await E(bridgeManager).register(BRIDGE_ID.VTRANSFER);
vtransferBridgeManager.reset();
vtransferBridgeManager.resolve(m);
kit = await provideKit(m);
await E(m).initHandler(kit.bridgeHandler);
console.info('Successfully initHandler for', BRIDGE_ID.VTRANSFER);
const vtransferBridge = await E(bridgeManager).register(vtransferID);
produceVtransferBridgeManager.reset();
produceVtransferBridgeManager.resolve(vtransferBridge);
bridgeTargetKit = await provideBridgeTargetKit(vtransferBridge);
await E(finisher).useRegistry(bridgeTargetKit.targetRegistry);
await E(vtransferBridge).initHandler(bridgeTargetKit.bridgeHandler);
console.info('Successfully initHandler for', vtransferID);
} catch (e) {
console.error(
'Failed to initHandler',
BRIDGE_ID.VTRANSFER,
vtransferID,
'reason:',
e,
'falling back to setHandler',
);
const m = await vtransferBridgeManagerP;
kit = await provideKit(m);
await E(m).setHandler(kit.bridgeHandler);
console.info('Successfully setHandler for', BRIDGE_ID.VTRANSFER);
const vtransferBridge = await vtransferBridgeManagerP;
bridgeTargetKit = await provideBridgeTargetKit(vtransferBridge);
await E(finisher).useRegistry(bridgeTargetKit.targetRegistry);
await E(vtransferBridge).setHandler(bridgeTargetKit.bridgeHandler);
console.info('Successfully setHandler for', vtransferID);
}

const newTransferMiddleware = await E(vats.transfer).makeTransferMiddleware(
kit,
);
transferMiddleware.reset();
transferMiddleware.resolve(newTransferMiddleware);
produceTransferMiddleware.reset();
produceTransferMiddleware.resolve(transferMiddleware);
};

export const getManifestForTransfer = (_powers, { transferRef }) => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/vats/src/proposals/vtransfer-echoer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { E } from '@endo/far';
/**
* @param {BootstrapPowers & {
* consume: {
* transferMiddleware: import('../vat-transfer.js').TransferMiddleware;
* transferMiddleware: import('../transfer.js').TransferMiddleware;
* };
* }} powers
* @param {object} options
Expand Down
Loading
Loading