Skip to content

Commit

Permalink
feat(ertp): Upgrade quotes to drop newly optional recovery sets
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Oct 2, 2023
1 parent da3b8a8 commit a7fb549
Show file tree
Hide file tree
Showing 11 changed files with 376 additions and 120 deletions.
181 changes: 169 additions & 12 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';

Expand All @@ -10,6 +10,8 @@ import { preparePaymentLedger } from './paymentLedger.js';

import './types-ambient.js';

// TODO Why does TypeScript lose the `MapStore` typing of `Baggage` here, even
// though it knows the correct type at the exporting `@agoric/vat-data`
/** @typedef {import('@agoric/vat-data').Baggage} Baggage */

/**
Expand All @@ -22,8 +24,12 @@ import './types-ambient.js';
*/

/**
* Used _only_ internally, to make a new issuerKit or to revive an old one.
*
* @template {AssetKind} K
* @param {IssuerRecord<K>} issuerRecord
* @param {RecoverySetsOption} recoverySetsState Omitted from issuerRecord
* because it was added in an upgrade.
* @param {Baggage} issuerBaggage
* @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in
* the middle of an atomic action (which btw should never happen), it
Expand All @@ -36,6 +42,7 @@ import './types-ambient.js';
*/
const setupIssuerKit = (
{ name, assetKind, displayInfo, elementShape },
recoverySetsState,
issuerBaggage,
optShutdownWithFailure = undefined,
) => {
Expand All @@ -61,6 +68,7 @@ const setupIssuerKit = (
assetKind,
cleanDisplayInfo,
elementShape,
recoverySetsState,
optShutdownWithFailure,
);

Expand All @@ -76,8 +84,17 @@ 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 an ancestor. See
* `RecoverySetsOption` for defaulting behavior.
*/
const RECOVERY_SETS_STATE = 'recoverySetsState';

/**
* Used _only_ to upgrade an ancestor issuerKit. Use `makeDurableIssuerKit` to
* make a new one.
*
* @template {AssetKind} K
* @param {Baggage} issuerBaggage
* @param {ShutdownWithFailure} [optShutdownWithFailure] If this issuer fails in
Expand All @@ -87,28 +104,77 @@ 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 prepareIssuerKit = (
export const upgradeIssuerKit = (
issuerBaggage,
optShutdownWithFailure = undefined,
recoverySetsOption = undefined,
) => {
const issuerRecord = issuerBaggage.get(INSTANCE_KEY);
return setupIssuerKit(issuerRecord, issuerBaggage, 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'`;
}
const recoverySetsState = recoverySetsOption || oldRecoverySetsState;
return setupIssuerKit(
issuerRecord,
issuerBaggage,
recoverySetsState,
optShutdownWithFailure,
);
};
harden(prepareIssuerKit);
harden(upgradeIssuerKit);

/**
* Does baggage already have an issuer from prepareIssuerKit()? That is: does it
* have the relevant keys defined?
* Confusingly, `prepareIssuerKit` was the original name for `upgradeIssuerKit`,
* even though it is used only to upgrade an ancestor issuerKit. Use
* `makeDurableIssuerKit` to make a new one.
*
* @deprecated Use `upgradeIssuerKit` instead if that's what you want. Or
* `reallyPrepareIssuerKit` if you want the behavior that should have been
* bound to this name.
*/
export const prepareIssuerKit = upgradeIssuerKit;

/**
* Does baggage already have an issuerKit?
*
* @param {Baggage} baggage
*/
export const hasIssuer = baggage => baggage.has(INSTANCE_KEY);

/** @typedef {Partial<{ elementShape: Pattern }>} IssuerOptionsRecord */
/**
* `elementShape`, may only be present for collection-style amounts. If present,
* it is a `Pattern` that every element of this issuerKits's amounts must
* satisfy. For example, the Zoe Invitation issuerKit uses an elementShape
* describing the invitation details for an individual invitation. An invitation
* purse or payment has an amount that can only be a set of these. (Though
* 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 an ancestor
* predating the introduction of recovery sets. See `RecoverySetsOption` for
* defaulting behavior.
*
* @typedef {Partial<{
* elementShape: Pattern;
* recoverySetsOption: RecoverySetsOption;
* }>} IssuerOptionsRecord
*/

/**
* Used _only_ to make a _new_ durable issuer, i.e., the initial incarnation of
* that issuer.
*
* @template {AssetKind} K The name becomes part of the brand in asset
* descriptions. The name is useful for debugging and double-checking
* assumptions, but should not be trusted wrt any external namespace. For
Expand Down Expand Up @@ -142,15 +208,106 @@ 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);
return setupIssuerKit(issuerData, issuerBaggage, optShutdownWithFailure);
const recoverySetsState = recoverySetsOption || 'hasRecoverySets';
issuerBaggage.init(RECOVERY_SETS_STATE, recoverySetsState);
return setupIssuerKit(
issuerData,
recoverySetsState,
issuerBaggage,
optShutdownWithFailure,
);
};
harden(makeDurableIssuerKit);

/**
* What _should_ have been named `prepareIssuerKit`. Used to either revive an
* ancestor issuer kit, or to make a new durable if it absent, and to place it
* in baggage for the next successor.
*
* @template {AssetKind} K The name becomes part of the brand in asset
* descriptions. The name is useful for debugging and double-checking
* assumptions, but should not be trusted wrt any external namespace. For
* example, anyone could create a new issuer kit with name 'BTC', but it is
* not bitcoin or even related. It is only the name according to that issuer
* and brand.
*
* The assetKind will be used to import a specific mathHelpers from the
* mathHelpers library. For example, natMathHelpers, the default, is used for
* basic fungible tokens.
*
* `displayInfo` gives information to the UI on how to display the amount.
* @param {Baggage} issuerBaggage
* @param {string} name
* @param {K} [assetKind]
* @param {AdditionalDisplayInfo} [displayInfo]
* @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
* provided, then the failed atomic action will call it, so that some larger
* 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 {IssuerOptionsRecord} [options]
* @returns {IssuerKit<K>}
*/
export const reallyPrepareIssuerKit = (
issuerBaggage,
name,
// @ts-expect-error K could be instantiated with a different subtype of AssetKind
assetKind = AssetKind.NAT,
displayInfo = harden({}),
optShutdownWithFailure = undefined,
options = {},
) => {
if (hasIssuer(issuerBaggage)) {
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. 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;
} else {
const issuerKit = makeDurableIssuerKit(
issuerBaggage,
name,
assetKind,
displayInfo,
optShutdownWithFailure,
options,
);
return issuerKit;
}
};
harden(reallyPrepareIssuerKit);

/**
* Used _only_ to make a new issuerKit that is effectively non-durable. This is
* currently done by making a durable one in a baggage not reachable from
* anywhere. TODO Once rebuilt on zones, this should instead just build on the
* virtual zone. See https://github.com/Agoric/agoric-sdk/pull/7116
*
* Currently used for testing only. Should probably continue to be used for
* testing only.
*
* @template {AssetKind} [K='nat'] The name becomes part of the brand in asset
* descriptions. The name is useful for debugging and double-checking
* assumptions, but should not be trusted wrt any external namespace. For
Expand Down Expand Up @@ -182,14 +339,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);
34 changes: 26 additions & 8 deletions packages/ERTP/src/paymentLedger.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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 @@ -88,6 +89,7 @@ export const preparePaymentLedger = (
assetKind,
displayInfo,
elementShape,
recoverySetsState,
optShutdownWithFailure = undefined,
) => {
/** @type {Brand<K>} */
Expand Down Expand Up @@ -145,6 +147,18 @@ export const preparePaymentLedger = (
{ valueShape: amountShape },
);

if (
recoverySetsState === 'noRecoverySets' &&
issuerBaggage.has('paymentRecoverySets')
) {
// Upgrade to `'noRecoverySets'` by dropping this all at once.
// Depending on conditions elsewhere, this may result in a ton of
// payments becoming unreachable all at once. Depend on (TODO upcoming)
// SwingSet support for incrementalizing the resulting deallocation
// work.
issuerBaggage.delete('paymentRecoverySets');
}

/**
* A withdrawn live payment is associated with the recovery set of the purse
* it was withdrawn from. Let's call these "recoverable" payments. All
Expand All @@ -162,12 +176,12 @@ export const preparePaymentLedger = (
* - A purse's recovery set only contains payments withdrawn from that purse and
* not yet consumed.
*
* @type {WeakMapStore<Payment, SetStore<Payment>>}
* @type {WeakMapStore<Payment, SetStore<Payment>> | undefined}
*/
const paymentRecoverySets = provideDurableWeakMapStore(
issuerBaggage,
'paymentRecoverySets',
);
const paymentRecoverySets =
recoverySetsState === 'noRecoverySets'
? undefined
: provideDurableWeakMapStore(issuerBaggage, 'paymentRecoverySets');

/**
* To maintain the invariants listed in the `paymentRecoverySets` comment,
Expand All @@ -179,6 +193,7 @@ export const preparePaymentLedger = (
*/
const initPayment = (payment, amount, optRecoverySet = undefined) => {
if (optRecoverySet !== undefined) {
assert(paymentRecoverySets !== undefined);
optRecoverySet.add(payment);
paymentRecoverySets.init(payment, optRecoverySet);
}
Expand All @@ -193,7 +208,7 @@ export const preparePaymentLedger = (
*/
const deletePayment = payment => {
paymentLedger.delete(payment);
if (paymentRecoverySets.has(payment)) {
if (paymentRecoverySets !== undefined && paymentRecoverySets.has(payment)) {
const recoverySet = paymentRecoverySets.get(payment);
paymentRecoverySets.delete(payment);
recoverySet.delete(payment);
Expand Down Expand Up @@ -283,14 +298,14 @@ export const preparePaymentLedger = (
* @param {(newPurseBalance: Amount) => void} updatePurseBalance - commit the
* purse balance
* @param {Amount} amount - the amount to be withdrawn
* @param {SetStore<Payment>} recoverySet
* @param {SetStore<Payment>} [recoverySet]
* @returns {Payment}
*/
const withdrawInternal = (
currentBalance,
updatePurseBalance,
amount,
recoverySet,
recoverySet = undefined,
) => {
amount = coerce(amount);
AmountMath.isGTE(currentBalance, amount) ||
Expand All @@ -310,6 +325,8 @@ export const preparePaymentLedger = (
return payment;
};

/** @type {() => Purse<K>} */
// @ts-expect-error type parameter confusion
const makeEmptyPurse = preparePurseKind(
issuerBaggage,
name,
Expand All @@ -320,6 +337,7 @@ export const preparePaymentLedger = (
depositInternal,
withdrawInternal,
}),
recoverySetsState,
);

/** @type {Issuer<K>} */
Expand Down
Loading

0 comments on commit a7fb549

Please sign in to comment.