Skip to content

Commit

Permalink
Merge pull request #9013 from Agoric/8400-recoverySets-nonIncremental
Browse files Browse the repository at this point in the history
Make Recovery sets optional
  • Loading branch information
mergify[bot] authored Mar 1, 2024
2 parents 7a91d1f + aa4b7db commit de159f2
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 27 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);
27 changes: 19 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,11 @@ export const preparePaymentLedger = (
* @param {SetStore<Payment>} [optRecoverySet]
*/
const initPayment = (payment, amount, optRecoverySet = undefined) => {
if (optRecoverySet !== undefined) {
if (recoverySetsState === 'noRecoverySets') {
optRecoverySet === undefined ||
Fail`when recoverSetsState === 'noRecoverySets', optRecoverySet must be empty`;
}
if (optRecoverySet !== undefined && !AmountMath.isEmpty(amount)) {
optRecoverySet.add(payment);
paymentRecoverySets.init(payment, optRecoverySet);
}
Expand Down Expand Up @@ -263,10 +272,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 Expand Up @@ -294,6 +303,8 @@ export const preparePaymentLedger = (
depositInternal,
withdrawInternal,
}),
recoverySetsState,
paymentRecoverySets,
);

/** @type {Issuer<K>} */
Expand Down
55 changes: 50 additions & 5 deletions packages/ERTP/src/purse.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { M } from '@agoric/store';
import { M, makeCopySet } from '@agoric/store';
import { AmountMath } from './amountMath.js';
import { makeTransientNotifierKit } from './transientNotifier.js';
import { makeAmountStore } from './amountStore.js';

const { Fail } = assert;

const EMPTY_COPY_SET = makeCopySet([]);

// TODO Type InterfaceGuard better than InterfaceGuard<any>
/**
* @param {import('@agoric/zone').Zone} issuerZone
Expand All @@ -19,6 +21,8 @@ const { Fail } = assert;
* depositInternal: any;
* withdrawInternal: any;
* }} purseMethods
* @param {RecoverySetsOption} recoverySetsState
* @param {WeakMapStore<Payment, SetStore<Payment>>} paymentRecoverySets
*/
export const preparePurseKind = (
issuerZone,
Expand All @@ -27,6 +31,8 @@ export const preparePurseKind = (
brand,
PurseIKit,
purseMethods,
recoverySetsState,
paymentRecoverySets,
) => {
const amountShape = brand.getAmountShape();

Expand All @@ -35,6 +41,34 @@ export const preparePurseKind = (
// TODO propagate zonifying to notifiers, maybe?
const { provideNotifier, update: updateBalance } = makeTransientNotifierKit();

/**
* If `recoverySetsState === 'hasRecoverySets'` (the normal state), then just
* return `state.recoverySet`.
*
* If `recoverySetsState === 'noRecoverySets'`, return `undefined`. Callers
* must be aware that the `undefined` return happens iff `recoverySetsState
* === 'noRecoverySets'`, and to avoid storing or retrieving anything from the
* actual recovery set.
*
* @param {{ recoverySet: SetStore<Payment> }} state
* @returns {SetStore<Payment> | undefined}
*/
const maybeRecoverySet = state => {
const { recoverySet } = state;
if (recoverySetsState === 'hasRecoverySets') {
return recoverySet;
} else {
recoverySetsState === 'noRecoverySets' ||
Fail`recoverSetsState must be noRecoverySets if it isn't hasRecoverSets`;
paymentRecoverySets !== undefined ||
Fail`paymentRecoverySets must always be defined`;
recoverySet.getSize() === 0 ||
Fail`With noRecoverySets, recoverySet must be empty`;

return undefined;
}
};

// - This kind is a pair of purse and depositFacet that have a 1:1
// correspondence.
// - They are virtualized together to share a single state record.
Expand Down Expand Up @@ -76,12 +110,14 @@ export const preparePurseKind = (
withdraw(amount) {
const { state } = this;
const { purse } = this.facets;

const optRecoverySet = maybeRecoverySet(state);
const balanceStore = makeAmountStore(state, 'currentBalance');
// Note COMMIT POINT within withdraw.
const payment = withdrawInternal(
balanceStore,
amount,
state.recoverySet,
optRecoverySet,
);
updateBalance(purse, balanceStore.getAmount());
return payment;
Expand All @@ -103,18 +139,27 @@ export const preparePurseKind = (
},

getRecoverySet() {
return this.state.recoverySet.snapshot();
const { state } = this;
const optRecoverySet = maybeRecoverySet(state);
if (optRecoverySet === undefined) {
return EMPTY_COPY_SET;
}
return optRecoverySet.snapshot();
},
recoverAll() {
const { state, facets } = this;
let amount = AmountMath.makeEmpty(brand, assetKind);
for (const payment of state.recoverySet.keys()) {
const optRecoverySet = maybeRecoverySet(state);
if (optRecoverySet === undefined) {
return amount; // empty at this time
}
for (const payment of optRecoverySet.keys()) {
// This does cause deletions from the set while iterating,
// but this special case is allowed.
const delta = facets.purse.deposit(payment);
amount = AmountMath.add(amount, delta, brand);
}
state.recoverySet.getSize() === 0 ||
optRecoverySet.getSize() === 0 ||
Fail`internal: Remaining unrecovered payments: ${facets.purse.getRecoverySet()}`;
return amount;
},
Expand Down
Loading

0 comments on commit de159f2

Please sign in to comment.