Skip to content

Commit

Permalink
feat: allow creators/revivors of Issuers to decline recoverySets
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris-Hibbert committed Feb 29, 2024
1 parent 18d561f commit 120b2c2
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 22 deletions.
79 changes: 69 additions & 10 deletions packages/ERTP/src/issuerKit.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,6 +26,8 @@ import './types-ambient.js';
* @template {AssetKind} K
* @param {IssuerRecord<K>} 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
Expand All @@ -38,6 +40,7 @@ import './types-ambient.js';
const setupIssuerKit = (
{ name, assetKind, displayInfo, elementShape },
issuerZone,
recoverySetsState,
optShutdownWithFailure = undefined,
) => {
assert.typeof(name, 'string');
Expand All @@ -62,6 +65,7 @@ const setupIssuerKit = (
assetKind,
cleanDisplayInfo,
elementShape,
recoverySetsState,
optShutdownWithFailure,
);

Expand All @@ -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
Expand All @@ -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<K>}
*/
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);

Expand All @@ -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
*/

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -273,14 +332,14 @@ export const makeIssuerKit = (
assetKind = AssetKind.NAT,
displayInfo = harden({}),
optShutdownWithFailure = undefined,
{ elementShape = undefined } = {},
{ elementShape = undefined, recoverySetsOption = undefined } = {},
) =>
makeDurableIssuerKit(
makeScalarBigMapStore('dropped issuer kit', { durable: true }),
name,
assetKind,
displayInfo,
optShutdownWithFailure,
{ elementShape },
{ elementShape, recoverySetsOption },
);
harden(makeIssuerKit);
24 changes: 16 additions & 8 deletions packages/ERTP/src/paymentLedger.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const amountShapeFromElementShape = (brand, assetKind, elementShape) => {
* @param {K} assetKind
* @param {DisplayInfo<K>} displayInfo
* @param {Pattern} elementShape
* @param {RecoverySetsOption} recoverySetsState
* @param {ShutdownWithFailure} [optShutdownWithFailure]
* @returns {PaymentLedger<K>}
*/
Expand All @@ -81,6 +82,7 @@ export const preparePaymentLedger = (
assetKind,
displayInfo,
elementShape,
recoverySetsState,
optShutdownWithFailure = undefined,
) => {
/** @type {Brand<K>} */
Expand Down Expand Up @@ -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
Expand All @@ -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<Payment, SetStore<Payment>>}
*/
const paymentRecoverySets = issuerZone.weakMapStore('paymentRecoverySets');
Expand All @@ -170,7 +175,10 @@ export const preparePaymentLedger = (
* @param {SetStore<Payment>} [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);
}
Expand Down Expand Up @@ -263,10 +271,10 @@ export const preparePaymentLedger = (
*
* @param {import('./amountStore.js').AmountStore} balanceStore
* @param {Amount} amount - the amount to be withdrawn
* @param {SetStore<Payment>} recoverySet
* @param {SetStore<Payment>} [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
Expand Down
5 changes: 5 additions & 0 deletions packages/ERTP/src/purse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 27 additions & 4 deletions packages/ERTP/src/types-ambient.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@
* @template {AssetKind} [K=AssetKind]
* @typedef {object} PaymentLedger
* @property {Mint<K>} mint
* @property {Purse<K>} mintRecoveryPurse Useful only to get the recovery set
* associated with minted payments that are still live.
* @property {Purse<K>} 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<K>} issuer
* @property {Brand<K>} brand
*/
Expand All @@ -183,8 +184,9 @@
* @template {AssetKind} [K=AssetKind]
* @typedef {object} IssuerKit
* @property {Mint<K>} mint
* @property {Purse<K>} mintRecoveryPurse Useful only to get the recovery set
* associated with minted payments that are still live.
* @property {Purse<K>} 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<K>} issuer
* @property {Brand<K>} brand
* @property {DisplayInfo} displayInfo
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<K>} 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.
*/

/**
Expand Down

0 comments on commit 120b2c2

Please sign in to comment.