diff --git a/packages/ERTP/src/issuerKit.js b/packages/ERTP/src/issuerKit.js index 65cce4993c5e..3aaa0ddfbe09 100644 --- a/packages/ERTP/src/issuerKit.js +++ b/packages/ERTP/src/issuerKit.js @@ -1,6 +1,6 @@ // @jessie-check -import { assert } from '@agoric/assert'; +import { assert, Fail } from '@agoric/assert'; import { assertPattern } from '@agoric/store'; import { makeScalarBigMapStore } from '@agoric/vat-data'; import { makeDurableZone } from '@agoric/zone/durable.js'; @@ -26,6 +26,8 @@ import './types-ambient.js'; * @template {AssetKind} K * @param {IssuerRecord} issuerRecord * @param {import('@agoric/zone').Zone} issuerZone + * @param {RecoverySetsOption} recoverySetsState Omitted from issuerRecord + * because it was added in an upgrade. * @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in * the middle of an atomic action (which btw should never happen), it * potentially leaves its ledger in a corrupted state. If this function was @@ -38,6 +40,7 @@ import './types-ambient.js'; const setupIssuerKit = ( { name, assetKind, displayInfo, elementShape }, issuerZone, + recoverySetsState, optShutdownWithFailure = undefined, ) => { assert.typeof(name, 'string'); @@ -62,6 +65,7 @@ const setupIssuerKit = ( assetKind, cleanDisplayInfo, elementShape, + recoverySetsState, optShutdownWithFailure, ); @@ -77,6 +81,12 @@ harden(setupIssuerKit); /** The key at which the issuer record is stored. */ const INSTANCE_KEY = 'issuer'; +/** + * The key at which the issuerKit's `RecoverySetsOption` state is stored. + * Introduced by an upgrade, so may be absent on a predecessor incarnation. See + * `RecoverySetsOption` for defaulting behavior. + */ +const RECOVERY_SETS_STATE = 'recoverySetsState'; /** * Used _only_ to upgrade a predecessor issuerKit. Use `makeDurableIssuerKit` to @@ -91,15 +101,39 @@ const INSTANCE_KEY = 'issuer'; * unit of computation, like the enclosing vat, can be shutdown before * anything else is corrupted by that corrupted state. See * https://github.com/Agoric/agoric-sdk/issues/3434 + * @param {RecoverySetsOption} [recoverySetsOption] Added in upgrade, so last + * and optional. See `RecoverySetsOption` for defaulting behavior. * @returns {IssuerKit} */ export const upgradeIssuerKit = ( issuerBaggage, optShutdownWithFailure = undefined, + recoverySetsOption = undefined, ) => { const issuerRecord = issuerBaggage.get(INSTANCE_KEY); const issuerZone = makeDurableZone(issuerBaggage); - return setupIssuerKit(issuerRecord, issuerZone, optShutdownWithFailure); + const oldRecoverySetsState = issuerBaggage.has(RECOVERY_SETS_STATE) + ? issuerBaggage.get(RECOVERY_SETS_STATE) + : 'hasRecoverySets'; + if ( + oldRecoverySetsState === 'noRecoverySets' && + recoverySetsOption === 'hasRecoverySets' + ) { + Fail`Cannot (yet?) upgrade from 'noRecoverySets' to 'hasRecoverySets'`; + } + if ( + oldRecoverySetsState === 'hasRecoverySets' && + recoverySetsOption === 'noRecoverySets' + ) { + Fail`Cannot (yet?) upgrade from 'hasRecoverySets' to 'noRecoverySets'`; + } + const recoverySetsState = recoverySetsOption || oldRecoverySetsState; + return setupIssuerKit( + issuerRecord, + issuerZone, + recoverySetsState, + optShutdownWithFailure, + ); }; harden(upgradeIssuerKit); @@ -119,8 +153,14 @@ export const hasIssuer = baggage => baggage.has(INSTANCE_KEY); * typically, the amount of an invitation payment is a singleton set. Such a * payment is often referred to in the singular as "an invitation".) * + * `recoverySetsOption` added in upgrade. Note that `IssuerOptionsRecord` is + * never stored, so we never need to worry about inheriting one from a + * predecessor predating the introduction of recovery sets. See + * `RecoverySetsOption` for defaulting behavior. + * * @typedef {Partial<{ * elementShape: Pattern; + * recoverySetsOption: RecoverySetsOption; * }>} IssuerOptionsRecord */ @@ -161,12 +201,24 @@ export const makeDurableIssuerKit = ( assetKind = AssetKind.NAT, displayInfo = harden({}), optShutdownWithFailure = undefined, - { elementShape = undefined } = {}, + { elementShape = undefined, recoverySetsOption = undefined } = {}, ) => { - const issuerData = harden({ name, assetKind, displayInfo, elementShape }); + const issuerData = harden({ + name, + assetKind, + displayInfo, + elementShape, + }); issuerBaggage.init(INSTANCE_KEY, issuerData); const issuerZone = makeDurableZone(issuerBaggage); - return setupIssuerKit(issuerData, issuerZone, optShutdownWithFailure); + const recoverySetsState = recoverySetsOption || 'hasRecoverySets'; + issuerBaggage.init(RECOVERY_SETS_STATE, recoverySetsState); + return setupIssuerKit( + issuerData, + issuerZone, + recoverySetsState, + optShutdownWithFailure, + ); }; harden(makeDurableIssuerKit); @@ -210,12 +262,19 @@ export const prepareIssuerKit = ( options = {}, ) => { if (hasIssuer(issuerBaggage)) { - const { elementShape: _ = undefined } = options; - const issuerKit = upgradeIssuerKit(issuerBaggage, optShutdownWithFailure); + const { elementShape: _ = undefined, recoverySetsOption = undefined } = + options; + const issuerKit = upgradeIssuerKit( + issuerBaggage, + optShutdownWithFailure, + recoverySetsOption, + ); // TODO check consistency with name, assetKind, displayInfo, elementShape. // Consistency either means that these are the same, or that they differ - // in a direction we are prepared to upgrade. + // in a direction we are prepared to upgrade. Note that it is the + // responsibility of `upgradeIssuerKit` to check consistency of + // `recoverySetsOption`, so continue to not do that here. // @ts-expect-error Type parameter confusion. return issuerKit; @@ -273,7 +332,7 @@ export const makeIssuerKit = ( assetKind = AssetKind.NAT, displayInfo = harden({}), optShutdownWithFailure = undefined, - { elementShape = undefined } = {}, + { elementShape = undefined, recoverySetsOption = undefined } = {}, ) => makeDurableIssuerKit( makeScalarBigMapStore('dropped issuer kit', { durable: true }), @@ -281,6 +340,6 @@ export const makeIssuerKit = ( assetKind, displayInfo, optShutdownWithFailure, - { elementShape }, + { elementShape, recoverySetsOption }, ); harden(makeIssuerKit); diff --git a/packages/ERTP/src/paymentLedger.js b/packages/ERTP/src/paymentLedger.js index bfa57b74f53d..9f6256e20749 100644 --- a/packages/ERTP/src/paymentLedger.js +++ b/packages/ERTP/src/paymentLedger.js @@ -72,6 +72,7 @@ const amountShapeFromElementShape = (brand, assetKind, elementShape) => { * @param {K} assetKind * @param {DisplayInfo} displayInfo * @param {Pattern} elementShape + * @param {RecoverySetsOption} recoverySetsState * @param {ShutdownWithFailure} [optShutdownWithFailure] * @returns {PaymentLedger} */ @@ -81,6 +82,7 @@ export const preparePaymentLedger = ( assetKind, displayInfo, elementShape, + recoverySetsState, optShutdownWithFailure = undefined, ) => { /** @type {Brand} */ @@ -141,11 +143,11 @@ export const preparePaymentLedger = ( }); /** - * A withdrawn live payment is associated with the recovery set of the purse - * it was withdrawn from. Let's call these "recoverable" payments. All - * recoverable payments are live, but not all live payments are recoverable. - * We do the bookkeeping for payment recovery with this weakmap from - * recoverable payments to the recovery set they are in. A bunch of + * A (non-empty) withdrawn live payment is associated with the recovery set of + * the purse it was withdrawn from. Let's call these "recoverable" payments. + * All recoverable payments are live, but not all live payments are + * recoverable. We do the bookkeeping for payment recovery with this weakmap + * from recoverable payments to the recovery set they are in. A bunch of * interesting invariants here: * * - Every payment that is a key in the outer `paymentRecoverySets` weakMap is @@ -157,6 +159,9 @@ export const preparePaymentLedger = ( * - A purse's recovery set only contains payments withdrawn from that purse and * not yet consumed. * + * If `recoverySetsState === 'noRecoverySets'`, then nothing should ever be + * added to this WeakStore. + * * @type {WeakMapStore>} */ const paymentRecoverySets = issuerZone.weakMapStore('paymentRecoverySets'); @@ -170,7 +175,10 @@ export const preparePaymentLedger = ( * @param {SetStore} [optRecoverySet] */ const initPayment = (payment, amount, optRecoverySet = undefined) => { - if (optRecoverySet !== undefined) { + if (recoverySetsState === 'noRecoverySets') { + assert(optRecoverySet === undefined); + } + if (optRecoverySet !== undefined && !AmountMath.isEmpty(amount)) { optRecoverySet.add(payment); paymentRecoverySets.init(payment, optRecoverySet); } @@ -263,10 +271,10 @@ export const preparePaymentLedger = ( * * @param {import('./amountStore.js').AmountStore} balanceStore * @param {Amount} amount - the amount to be withdrawn - * @param {SetStore} recoverySet + * @param {SetStore} [recoverySet] * @returns {Payment} */ - const withdrawInternal = (balanceStore, amount, recoverySet) => { + const withdrawInternal = (balanceStore, amount, recoverySet = undefined) => { amount = coerce(amount); const payment = makePayment(); // COMMIT POINT Move the withdrawn assets from this purse into diff --git a/packages/ERTP/src/purse.js b/packages/ERTP/src/purse.js index e8cafa50fa56..bc5cbcaf1112 100644 --- a/packages/ERTP/src/purse.js +++ b/packages/ERTP/src/purse.js @@ -108,6 +108,11 @@ export const preparePurseKind = ( recoverAll() { const { state, facets } = this; let amount = AmountMath.makeEmpty(brand, assetKind); + if (state.recoverySet === undefined) { + // Note that even this case does only the gc work implied by the + // call to `cleanerRecoverySet` above. + return amount; // empty at this time + } for (const payment of state.recoverySet.keys()) { // This does cause deletions from the set while iterating, // but this special case is allowed. diff --git a/packages/ERTP/src/types-ambient.js b/packages/ERTP/src/types-ambient.js index 59c4388b7d06..24e065469adc 100644 --- a/packages/ERTP/src/types-ambient.js +++ b/packages/ERTP/src/types-ambient.js @@ -173,8 +173,9 @@ * @template {AssetKind} [K=AssetKind] * @typedef {object} PaymentLedger * @property {Mint} mint - * @property {Purse} mintRecoveryPurse Useful only to get the recovery set - * associated with minted payments that are still live. + * @property {Purse} mintRecoveryPurse Externally useful only if this issuer + * uses recovery sets. Can be used to get the recovery set associated with + * minted payments that are still live. * @property {Issuer} issuer * @property {Brand} brand */ @@ -183,8 +184,9 @@ * @template {AssetKind} [K=AssetKind] * @typedef {object} IssuerKit * @property {Mint} mint - * @property {Purse} mintRecoveryPurse Useful only to get the recovery set - * associated with minted payments that are still live. + * @property {Purse} mintRecoveryPurse Externally useful only if this issuer + * uses recovery sets. Can be used to get the recovery set associated with + * minted payments that are still live. * @property {Issuer} issuer * @property {Brand} brand * @property {DisplayInfo} displayInfo @@ -217,6 +219,23 @@ // /////////////////////////// Purse / Payment ///////////////////////////////// +/** + * Issuers first became durable with mandatory recovery sets. Later they were + * made optional, but there is no support for converting from one state to the + * other. Thus, absence of a `RecoverySetsOption` state is equivalent to + * `'hasRecoverySets'`. In the absence of a `recoverySetsOption` parameter, + * upgradeIssuerKit defaults to the predecessor's `RecoverySetsOption` state, or + * `'hasRecoverySets'` if none. + * + * At this time, a `'noRecoverySets'` predecessor cannot be upgraded to a + * `'hasRecoverySets'` successor. If it turns out this transition is needed, it + * can likely be supported in a future upgrade. + * + * @typedef {'hasRecoverySets' | 'noRecoverySets'} RecoverySetsOption + */ + +// /////////////////////////// Purse / Payment ///////////////////////////////// + /** * @callback DepositFacetReceive * @param {Payment} payment @@ -276,10 +295,14 @@ * can spend the assets at stake on other things. Afterwards, if the recipient * of the original check finally gets around to depositing it, their deposit * fails. + * + * Returns an empty set if this issuer does not support recovery sets. * @property {() => Amount} recoverAll For use in emergencies, such as coming * back from a traumatic crash and upgrade. This deposits all the payments in * this purse's recovery set into the purse itself, returning the total amount * of assets recovered. + * + * Returns an empty amount if this issuer does not support recovery sets. */ /**