Skip to content

Commit

Permalink
feat(zoe): Make zcf singleton durable (#9531)
Browse files Browse the repository at this point in the history
Staged on #9533 

refs: #9281 

## Description

The `zcf` object will effectively need to be passed through `orchestrate` as an endowment. Because zcf is not durable, or even an exo, we were originally planning to do it with a mechanism involving a standing durable object, and then wrap and unwrap it on either side of the membrane. But if `zcf` were durable, we wouldn't need all this complexity. It turns out, if this PR is correct, that making `zcf` durable is trivial.

### Security Considerations

Making `zcf` into a durable exo also involves giving it an interface guard. The interface guard in the first commit of this PR makes a needed exception for `makeInvitation` and `setTestJig` because both of them accept non-passable parameters. The `defaultGuards: 'passable'` option means that all other methods default to a guard that merely enforces that all arguments and return results are passable. This does make `zcf` somewhat more defensive, but not much.

Given this starting point, we can grow that `ZcfI` interface guard to do more explicit input validation of the other methods, which will help security, and make us less vulnerable to insufficient input validation in the zcf methods themselves. As we move more of the input validation to the method guards, we should be able to remove ad hoc input validation code in the method which has become redundant. Replacement of ad hoc input validation with declarative guard-based input validation should help security.

I don't yet know whether I'll grow the `ZcfI` interface guard to have these explicit method guards in further commits to this PR or in later PR.

### Scaling Considerations
The extra guard checks are potentially an issue, but we won't know until we profile.

### Documentation Considerations
none

### Testing Considerations

I need to understand `setTestJig` better.

### Upgrade Considerations

Making `zcf` durable means that it has a durable identity that survives upgrade. As a durable exo singleton, it is stateless, meaning that it gets back all the state it needs during `prepareExo` as state that its methods capture (close over) rather than as exo instance state. This reflects naturally the initial intuition that the `zcf` endowment, being stateless, could just be represented to `asyncFlow` as a singleton standin, re-endowed during the prepare phase.
  • Loading branch information
erights authored Jun 19, 2024
1 parent bf9f03b commit 62c73f5
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 20 deletions.
1 change: 1 addition & 0 deletions packages/zoe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@endo/import-bundle": "^1.1.2",
"@endo/marshal": "^1.5.0",
"@endo/nat": "^5.0.7",
"@endo/pass-style": "^1.4.0",
"@endo/patterns": "^1.4.0",
"@endo/promise-kit": "^1.1.2",
"yargs-parser": "^21.1.1"
Expand Down
25 changes: 25 additions & 0 deletions packages/zoe/src/contractFacet/typeGuards.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { M } from '@endo/patterns';
import { AmountKeywordRecordShape, IssuerRecordShape } from '../typeGuards.js';

export const ZcfSeatShape = M.remotable('zcfSeat');

export const ZcfMintI = M.interface('ZcfMint', {
getIssuerRecord: M.call().returns(IssuerRecordShape),
mintGains: M.call(AmountKeywordRecordShape)
.optional(ZcfSeatShape)
.returns(ZcfSeatShape),
burnLosses: M.call(AmountKeywordRecordShape, ZcfSeatShape).returns(),
});

export const ZcfI = M.interface(
'ZCF',
{
makeInvitation: M.call(M.raw(), M.string())
.optional(M.record(), M.pattern())
.returns(M.promise()),
setTestJig: M.call().optional(M.raw()).returns(),
},
{
defaultGuards: 'passable',
},
);
2 changes: 1 addition & 1 deletion packages/zoe/src/contractFacet/zcfMint.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { assertFullIssuerRecord, makeIssuerRecord } from '../issuerRecord.js';
import { addToAllocation, subtractFromAllocation } from './allocationMath.js';

import '../internal-types.js';
import { ZcfMintI } from '../typeGuards.js';
import { ZcfMintI } from './typeGuards.js';
import './internal-types.js';
import './types-ambient.js';

Expand Down
9 changes: 3 additions & 6 deletions packages/zoe/src/contractFacet/zcfZygote.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
provideDurableMapStore,
} from '@agoric/vat-data';
import { E } from '@endo/eventual-send';
import { passStyleOf, Remotable } from '@endo/marshal';
import { passStyleOf } from '@endo/pass-style';
import { makePromiseKit } from '@endo/promise-kit';

import { objectMap } from '@agoric/internal';
Expand All @@ -26,6 +26,7 @@ import { createSeatManager } from './zcfSeat.js';

import { HandleOfferI, InvitationHandleShape } from '../typeGuards.js';
import { prepareZcMint } from './zcfMint.js';
import { ZcfI } from './typeGuards.js';

/// <reference path="../internal-types.js" />
/// <reference path="./internal-types.js" />
Expand Down Expand Up @@ -281,11 +282,7 @@ export const makeZCFZygote = async (
['canBeUpgraded', 'canUpgrade'].includes(meta.upgradability);

/** @type {ZCF} */
// Using Remotable rather than Far because there are too many complications
// imposing checking wrappers: makeInvitation() and setJig() want to
// accept raw functions. assert cannot be a valid passable! (It's a function
// and has members.)
const zcf = Remotable('Alleged: zcf', undefined, {
const zcf = prepareExo(zcfBaggage, 'zcf', ZcfI, {
atomicRearrange: transfers => seatManager.atomicRearrange(transfers),
reallocate: (...seats) => seatManager.reallocate(...seats),
assertUniqueKeyword: kwd => getInstanceRecHolder().assertUniqueKeyword(kwd),
Expand Down
11 changes: 0 additions & 11 deletions packages/zoe/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,17 +144,6 @@ export const ZoeMintI = M.interface('ZoeMint', {
withdrawAndBurn: M.call(AmountShape).returns(),
});

export const ZcfMintI = M.interface('ZcfMint', {
getIssuerRecord: M.call().returns(IssuerRecordShape),
mintGains: M.call(AmountKeywordRecordShape)
.optional(M.remotable('zcfSeat'))
.returns(M.remotable('zcfSeat')),
burnLosses: M.call(
AmountKeywordRecordShape,
M.remotable('zcfSeat'),
).returns(),
});

export const FeeMintAccessShape = M.remotable('FeeMintAccess');

export const ExitObjectI = M.interface('Exit Object', {
Expand Down
6 changes: 4 additions & 2 deletions packages/zoe/test/unitTests/zcf/zcf.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,8 @@ test(`zcf.makeInvitation - no description`, async t => {
const { zcf } = await setupZCFTest();
// @ts-expect-error deliberate invalid arguments for testing
t.throws(() => zcf.makeInvitation(() => {}), {
message: 'invitations must have a description string: "[undefined]"',
message:
'In "makeInvitation" method of (zcf): Expected at least 2 arguments: ["<redacted raw arg>"]',
});
});

Expand All @@ -296,7 +297,8 @@ test(`zcf.makeInvitation - non-string description`, async t => {
// https://github.com/Agoric/agoric-sdk/issues/1704
// @ts-expect-error deliberate invalid arguments for testing
t.throws(() => zcf.makeInvitation(() => {}, { something: 'a' }), {
message: /invitations must have a description string: .*/,
message:
'In "makeInvitation" method of (zcf): arg 1: copyRecord {"something":"a"} - Must be a string',
});
});

Expand Down

0 comments on commit 62c73f5

Please sign in to comment.