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

feat(orchestration): ZCFTools #10057

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

feat(orchestration): ZCFTools #10057

wants to merge 1 commit into from

Conversation

dckc
Copy link
Member

@dckc dckc commented Sep 10, 2024

closes: #9773

Description

Provide selected ZCF APIs for use in orchestration flows. For example: use vows for resumable promises.

Security Considerations

nothing new

Scaling Considerations

n/a

Documentation Considerations

  • reference docs for attenuated ZCF

Testing Considerations

  • unit tests
  • a bit of integration with one of the examples

Upgrade Considerations

n/a

@dckc dckc requested review from turadg and mhofman September 10, 2024 20:35
Copy link
Member

@turadg turadg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to also update the orchestration facades to replace any full ZCF in a context to the attenuated one. Once that's done some of the existing example tests will cover this.

Copy link

cloudflare-workers-and-pages bot commented Sep 10, 2024

Deploying agoric-sdk with  Cloudflare Pages  Cloudflare Pages

Latest commit: d1e234f
Status: ✅  Deploy successful!
Preview URL: https://4eeeceb2.agoric-sdk.pages.dev
Branch Preview URL: https://9773-attenuated-zcf.agoric-sdk.pages.dev

View logs

@mhofman
Copy link
Member

mhofman commented Sep 11, 2024

  • A durable exo needs to be explicitly created from a heap zcf object.

I'm not quite sure how to do that. Maybe pass in zcf at prepare time? Is this thing supposed to be a singleton?

Yes, a singleton durable exo can be created whose behavior wraps over the singleton zcf object provided to the contract start function.

@mhofman
Copy link
Member

mhofman commented Sep 11, 2024

Do we establish that by inspection? Is it testable?

Good question. I'm actually wondering if patterns could help here. Effectively the new objects have to implement all previous methods, accepting at least the same arguments that were previously accepted. The return values must have at least the same fields that were previously present. TypeScript types might be able to help here, as this is effectively the definition of assignability.

packages/orchestration/src/facade.js Outdated Show resolved Hide resolved
packages/orchestration/src/exos/zcf-for-flow.js Outdated Show resolved Hide resolved
@@ -64,6 +65,8 @@ export const makeOrchestrationFacade = ({

const { prepareEndowment, asyncFlow } = asyncFlowTools;

const zcfLtd = prepareZcfForFlows(zcf, zone, vowTools);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs to be called by provideOrchestration using zones.orchestration (aka the system orchestration zone), and returned as one of the orchestration tools.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the impact on contracts?

I started making this change... it's straightforward enough... but it means that all the contracts don't use the attenuated ZCF unless we change them to opt-in. Is that by design?

How does this impact the 1st arg to the withOrchestration callback (usually contract)? Do we keep the normal zcf there plus the attenuated zcf in the tools?

const contract = async (zcf, _privateArgs, zone, tools) => { ... }
export const start = withOrchestration(contract);

@dckc dckc force-pushed the 9773-attenuated-zcf branch 3 times, most recently from 25a1716 to 0e3215c Compare September 12, 2024 22:28
@dckc dckc marked this pull request as ready for review September 12, 2024 22:28
@dckc dckc requested review from turadg and mhofman September 12, 2024 22:28

import { M } from '@endo/patterns';

export const ZcfI = M.interface(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is different than ZcfI exported from @agoric/zoe. Let's have a distinct name.

I'd suggest ZcfForFlowsI but that's quite clunky. But that goes for the whole Exo name imo.

Would FlowsZcf, FlowsZcfI and flowsZcf suffice? JSdoc could resolve any ambiguity

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One advantage of zcfForFlows is that it starts with zcf, and makes autocomplete simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorting with ZCF seems worthwhile. going with ZcfForFlowsI

Comment on lines 57 to 63
/**
* @deprecated
* @type {ZCF['reallocate']}
*/
reallocate(seat1, seat2, ...seatRest) {
return zcf.reallocate(seat1, seat2, ...seatRest);
},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to propagate this function on the chopping block

Suggested change
/**
* @deprecated
* @type {ZCF['reallocate']}
*/
reallocate(seat1, seat2, ...seatRest) {
return zcf.reallocate(seat1, seat2, ...seatRest);
},

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. it was specified in #9773, but I'm dropping it.

@@ -78,6 +78,11 @@ export const makeOrchestrationFacade = ({
const [wrappedCtx] = prepareEndowment(subZone, 'endowments', [hostCtx]);
const hostFn = asyncFlow(subZone, 'asyncFlow', guestFn);

deepMapObject(
wrappedCtx,
val => val === zcf && assert.fail('do not pass zcf'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slick

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the message point towards using zcfForFlows too?


test.before('set up context', async t => (t.context = await makeTestContext()));

test('yes as is: atomicRearrange(), ... getTerms()', async t => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took me a bit to understand the test name. consider,

  • removed
  • unchanged
  • changed:
  • changed:

packages/orchestration/test/exos/zcf-for-flows.test.ts Outdated Show resolved Hide resolved
Copy link
Member

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a small request on the makeInvitation guard. I hadn't realized why it had this shape in the first place.


import { M } from '@endo/patterns';

export const ZcfI = M.interface(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One advantage of zcfForFlows is that it starts with zcf, and makes autocomplete simpler.

@@ -78,6 +78,11 @@ export const makeOrchestrationFacade = ({
const [wrappedCtx] = prepareEndowment(subZone, 'endowments', [hostCtx]);
const hostFn = asyncFlow(subZone, 'asyncFlow', guestFn);

deepMapObject(
wrappedCtx,
val => val === zcf && assert.fail('do not pass zcf'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the message point towards using zcfForFlows too?

packages/orchestration/src/types.ts Show resolved Hide resolved
Comment on lines 83 to 91
test('makeInvitation: non-passable handler', async t => {
const { zoe, zcf, zcfLtd, vt } = t.context;

const toTradeVow = zcfLtd.makeInvitation(_seat => {}, 'trade');

const toTrade = await vt.when(toTradeVow);
const amt = await E(E(zoe).getInvitationIssuer()).getAmountOf(toTrade);
t.like(amt, { value: [{ description: 'trade' }] });
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary to test for as it's not explicitly supported. Only passable arguments without promises are supported by the async flow membrane, the attenuated zcf should never see such a thing. Let's actually make the guard stricter than the original zcf and test that it refuses a function handler.

export const ZcfI = M.interface(
'ZCF',
{
makeInvitation: M.call(M.raw(), M.string())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that the membrane does not support non-passable arguments, let's restrict to a handler in remotable form ?

Suggested change
makeInvitation: M.call(M.raw(), M.string())
makeInvitation: M.call(M.remotable('OfferHandler'), M.string())

Comment on lines 36 to 37
// TODO: figure out Guarded<ZcfForFlows> vs. ZcfForFlows
const { unbondAndLiquidStake } = orchestrateAll(flows, { zcfForFlows });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@turadg I'm still interested in help with the types here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look and the best fix isn't obvious to me. Some of the problem may be going from guest to host and back.

I would try defining the interface as a Guest interface (akin to ZCF) and making that what the flow takes. Then define the Exo in terms of that using HostOf.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I did something like that. Now it seems to be griping about brands etc. that come from getTerms() -- their isMyIssuer method returns a promise, and it wants vows for those. Clues?

@dckc dckc requested review from mhofman and turadg September 13, 2024 15:28
Copy link
Member

@mhofman mhofman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

packages/orchestration/src/types.ts Outdated Show resolved Hide resolved
@@ -26,6 +27,7 @@ export const ZcfForFlowsI = M.interface(
* @param {VowTools} vowTools
*/
export const prepareZcfForFlows = (zcf, zone, vowTools) => {
/** @satisfies {HostInterface<ZcfForFlows>} */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

some of the errors now are with the Host interface exposing methods that return promises. Should we excise those?

getTerms().issuers[0].isMyIssuer(): Promise<boolean>
makeEmptySeatKit(...).zcfSeat.getSubscriber(...): Promise<Subscriber<Allocation>>

I think otherwise they'll need durable watchers to become vows.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh yeah these will not be usable inside the membrane

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to drop getTerms() -- I don't see any need to delay calling it until a flow has started.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

excising getSubscriber means wrapping zcfSeat. I suppose that's reasonably straightforward.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait I'm confused, zcfSeat is a local or remote object?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type error does point to some runtime impact.

I just don't know how to remove methods from brands while preserving their identity, which is kinda the whole point of a brand

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think we can just remove them from the types so that they don't appear to be available. If someone goes and uses a method that the docs and static types say isn't there, they shouldn't be surprised that it fails. (This doesn't apply to when we need to attenuate powers, but that's not the case here. If the method worked we'd gladly include it.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just remove them from the types so that they don't appear to be available.

That was my original thought but the problem is that passing such an object back as argument where a zcf seat is expected would cause a type error because the object wouldn't be compatible

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attenuated ZCF should just be different from the ZCF. It just doesn't get past as an argument

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dtribble the problem is not ZCF but zcfSeat in the result of makeEmptySeatKit. Does that need to be passed back anywhere? Should some of its methods be usable from the flow?

@dckc dckc force-pushed the 9773-attenuated-zcf branch 2 times, most recently from fec4ab1 to 81b621a Compare September 19, 2024 18:35
@dckc dckc requested review from mhofman and turadg September 19, 2024 18:38
@dckc dckc changed the title feat(orchestration): attenuated ZCF feat(orchestration): ZCFTools Sep 19, 2024
@@ -78,6 +78,13 @@ export const makeOrchestrationFacade = ({
const [wrappedCtx] = prepareEndowment(subZone, 'endowments', [hostCtx]);
const hostFn = asyncFlow(subZone, 'asyncFlow', guestFn);

deepMapObject(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nb: the usage here somewhat abuses the "map" idea. Kind of a deepAssert or visitObject

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quite

packages/orchestration/src/types.ts Outdated Show resolved Hide resolved
Comment on lines 20 to 30
export type ZcfTools<CT = Record<string, unknown>> = Pick<
ZCF<CT>,
'atomicRearrange' | 'assertUniqueKeyword'
> & {
makeInvitation: <R, A = undefined>(
offerHandler: OfferHandler<ERef<R>, A>,
description: string,
customDetails?: object,
proposalShape?: Pattern,
) => Promise<Invitation<R, A>>;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use an actual interface. This doesn't need any parameterization because CT (Custom Terms) will not be available.

Suggested change
export type ZcfTools<CT = Record<string, unknown>> = Pick<
ZCF<CT>,
'atomicRearrange' | 'assertUniqueKeyword'
> & {
makeInvitation: <R, A = undefined>(
offerHandler: OfferHandler<ERef<R>, A>,
description: string,
customDetails?: object,
proposalShape?: Pattern,
) => Promise<Invitation<R, A>>;
};
export interface ZcfTools {
assertUniqueKeyword: ZCF['assertUniqueKeyword'];
atomicRearrange: ZCF['atomicRearrange'];
makeInvitation: ZCF['makeInvitation'];
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, an explicit interface like this makes more sense for when we start deviating

packages/orchestration/src/utils/zcf-tools.js Outdated Show resolved Hide resolved
packages/orchestration/src/utils/zcf-tools.js Show resolved Hide resolved
packages/orchestration/test/fixtures/zcfTester.contract.js Outdated Show resolved Hide resolved
packages/orchestration/src/types.ts Outdated Show resolved Hide resolved
packages/orchestration/src/types.ts Outdated Show resolved Hide resolved
Comment on lines 20 to 30
export type ZcfTools<CT = Record<string, unknown>> = Pick<
ZCF<CT>,
'atomicRearrange' | 'assertUniqueKeyword'
> & {
makeInvitation: <R, A = undefined>(
offerHandler: OfferHandler<ERef<R>, A>,
description: string,
customDetails?: object,
proposalShape?: Pattern,
) => Promise<Invitation<R, A>>;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah, an explicit interface like this makes more sense for when we start deviating

 - unit tests
   - zcfTester copied from @agoric/zoe
 - don't pass zcf thru context in examples
 - restrict makeInvitation handler to passable
 - regenerate baggage snapshots
@dckc dckc added the automerge:rebase Automatically rebase updates, then merge label Sep 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
automerge:rebase Automatically rebase updates, then merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Variant of ZCF for orchestrated flows
4 participants