From 62c73f5befa179579864c1586ec2dd5be080ba0b Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Wed, 19 Jun 2024 16:44:59 -0700 Subject: [PATCH] feat(zoe): Make zcf singleton durable (#9531) 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. --- packages/zoe/package.json | 1 + packages/zoe/src/contractFacet/typeGuards.js | 25 ++++++++++++++++++++ packages/zoe/src/contractFacet/zcfMint.js | 2 +- packages/zoe/src/contractFacet/zcfZygote.js | 9 +++---- packages/zoe/src/typeGuards.js | 11 --------- packages/zoe/test/unitTests/zcf/zcf.test.js | 6 +++-- 6 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 packages/zoe/src/contractFacet/typeGuards.js diff --git a/packages/zoe/package.json b/packages/zoe/package.json index bb33081e34c..d836ed12796 100644 --- a/packages/zoe/package.json +++ b/packages/zoe/package.json @@ -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" diff --git a/packages/zoe/src/contractFacet/typeGuards.js b/packages/zoe/src/contractFacet/typeGuards.js new file mode 100644 index 00000000000..044a283298e --- /dev/null +++ b/packages/zoe/src/contractFacet/typeGuards.js @@ -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', + }, +); diff --git a/packages/zoe/src/contractFacet/zcfMint.js b/packages/zoe/src/contractFacet/zcfMint.js index e6cf7833950..b8423125837 100644 --- a/packages/zoe/src/contractFacet/zcfMint.js +++ b/packages/zoe/src/contractFacet/zcfMint.js @@ -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'; diff --git a/packages/zoe/src/contractFacet/zcfZygote.js b/packages/zoe/src/contractFacet/zcfZygote.js index 49384f0b8b4..fd6c0b335fd 100644 --- a/packages/zoe/src/contractFacet/zcfZygote.js +++ b/packages/zoe/src/contractFacet/zcfZygote.js @@ -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'; @@ -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'; /// /// @@ -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), diff --git a/packages/zoe/src/typeGuards.js b/packages/zoe/src/typeGuards.js index e694725fc7b..7d2fd0664c3 100644 --- a/packages/zoe/src/typeGuards.js +++ b/packages/zoe/src/typeGuards.js @@ -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', { diff --git a/packages/zoe/test/unitTests/zcf/zcf.test.js b/packages/zoe/test/unitTests/zcf/zcf.test.js index 5f6900bb114..828d3757325 100644 --- a/packages/zoe/test/unitTests/zcf/zcf.test.js +++ b/packages/zoe/test/unitTests/zcf/zcf.test.js @@ -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: [""]', }); }); @@ -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', }); });