Skip to content

Commit

Permalink
Zoe use watchPromises for the release branch (#8710)
Browse files Browse the repository at this point in the history
* feat: zcf ask zoe to repair 'adminNode.done()` watcher on restart

use watchPromise() to wait for contract finish; This repairs new
contracts, but doesn't help existing contracts.

* test: add tests for userSeat.getExitSubscriber()

* test: repair broken test of upgrade

There was a bug in the part of the test that was attempting to run a
bunch of scenarios across the upgrade. Rather than resuming each run
after the upgrade, it was running the whole scenario from scratch
after the upgrade.

* test: demonstrate Zoe's survival through vatAdmmin vat upgrade

* refactor: upgrade-zoe proposal name

it's not necessarily null. it's whatever the version is that's built.

---------

Co-authored-by: Turadg Aleahmad <[email protected]>
  • Loading branch information
Chris-Hibbert and turadg authored Jan 4, 2024
1 parent 7f58a76 commit d141183
Show file tree
Hide file tree
Showing 7 changed files with 466 additions and 16 deletions.
335 changes: 335 additions & 0 deletions packages/boot/test/bootstrapTests/test-walletSurvivesZoeRestart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
/** @file Bootstrap test of liquidation across multiple collaterals */
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import { NonNullish } from '@agoric/assert';
import process from 'process';
import type { ExecutionContext, TestFn } from 'ava';
import type { ScheduleNotification } from '@agoric/inter-protocol/src/auction/scheduler.js';
import { BridgeHandler } from '@agoric/vats';
import {
LiquidationTestContext,
likePayouts,
makeLiquidationTestContext,
scale6,
LiquidationSetup,
} from '../../tools/liquidation.ts';

const test = anyTest as TestFn<LiquidationTestContext>;

//#region Product spec
const setup: LiquidationSetup = {
vaults: [
{
atom: 15,
ist: 100,
debt: 100.5,
},
{
atom: 15,
ist: 103,
debt: 103.515,
},
{
atom: 15,
ist: 105,
debt: 105.525,
},
],
bids: [
{
give: '80IST',
discount: 0.1,
},
{
give: '90IST',
price: 9.0,
},
{
give: '150IST',
discount: 0.15,
},
],
price: {
starting: 12.34,
trigger: 9.99,
},
auction: {
start: {
collateral: 45,
debt: 309.54,
},
end: {
collateral: 9.659301,
debt: 0,
},
},
};

const outcome = {
bids: [
{
payouts: {
Bid: 0,
Collateral: 8.897786,
},
},
{
payouts: {
Bid: 0,
Collateral: 10.01001,
},
},
{
payouts: {
Bid: 10.46,
Collateral: 16.432903,
},
},
],
reserve: {
allocations: {
ATOM: 0.309852,
STARS: 0.309852,
},
shortfall: 0,
},
vaultsSpec: [
{
locked: 3.373,
},
{
locked: 3.024,
},
{
locked: 2.792,
},
],
// TODO match spec https://github.com/Agoric/agoric-sdk/issues/7837
vaultsActual: [
{
locked: 3.525747,
},
{
locked: 3.181519,
},
{
locked: 2.642185,
},
],
} as const;
//#endregion

test.before(async t => {
t.context = await makeLiquidationTestContext(t);
});
test.after.always(t => {
return t.context.shutdown && t.context.shutdown();
});

// Reference: Flow 1 from https://github.com/Agoric/agoric-sdk/issues/7123
const checkFlow1 = async (
t: ExecutionContext<LiquidationTestContext>,
{
collateralBrandKey,
managerIndex,
}: { collateralBrandKey: string; managerIndex: number },
_expected: any,
) => {
// fail if there are any unhandled rejections
process.on('unhandledRejection', (error: Error) => {
t.fail(error.message);
});

const {
advanceTimeBy,
advanceTimeTo,
check,
priceFeedDrivers,
readLatest,
walletFactoryDriver,
setupVaults,
placeBids,
controller,
buildProposal,
} = t.context;
const { EV } = t.context.runUtils;

const buildAndExecuteProposal = async packageSpec => {
const proposal = await buildProposal(packageSpec);

for await (const bundle of proposal.bundles) {
await controller.validateAndInstallBundle(bundle);
}

const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.evals,
};

const coreEvalBridgeHandler: ERef<BridgeHandler> = await EV.vat(
'bootstrap',
).consumeItem('coreEvalBridgeHandler');
await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage);
};

const metricsPath = `published.vaultFactory.managers.manager${managerIndex}.metrics`;

await setupVaults(collateralBrandKey, managerIndex, setup);

const buyer = await walletFactoryDriver.provideSmartWallet('agoric1buyer');
await placeBids(collateralBrandKey, 'agoric1buyer', setup);

{
// ---------------
// Change price to trigger liquidation
// ---------------

await priceFeedDrivers[collateralBrandKey].setPrice(9.99);

// check nothing liquidating yet
const liveSchedule: ScheduleNotification = readLatest(
'published.auction.schedule',
);
t.is(liveSchedule.activeStartTime, null);
t.like(readLatest(metricsPath), {
numActiveVaults: setup.vaults.length,
numLiquidatingVaults: 0,
});

// advance time to start an auction
console.log(collateralBrandKey, 'step 1 of 10');
await advanceTimeTo(NonNullish(liveSchedule.nextDescendingStepTime));
t.like(readLatest(metricsPath), {
numActiveVaults: 0,
numLiquidatingVaults: setup.vaults.length,
liquidatingCollateral: {
value: scale6(setup.auction.start.collateral),
},
liquidatingDebt: { value: scale6(setup.auction.start.debt) },
lockedQuote: null,
});

console.log(collateralBrandKey, 'step 2 of 10');
await advanceTimeBy(3, 'minutes');
t.like(readLatest(`published.auction.book${managerIndex}`), {
collateralAvailable: { value: scale6(setup.auction.start.collateral) },
startCollateral: { value: scale6(setup.auction.start.collateral) },
startProceedsGoal: { value: scale6(setup.auction.start.debt) },
});

console.log(collateralBrandKey, 'step 3 of 10');
await advanceTimeBy(3, 'minutes');

console.log(collateralBrandKey, 'step 4 of 10');
await advanceTimeBy(3, 'minutes');
// XXX updates for bid1 and bid2 are appended in the same turn so readLatest gives bid2
// NB: console output shows 8897786n payout which matches spec 8.897ATOM
// t.like(readLatest('published.wallet.agoric1buyer'), {
// status: {
// id: `${collateralBrandKey}-bid1`,
// payouts: {
// Bid: { value: 0n },
// Collateral: { value: scale6(outcome.bids[0].payouts.Collateral) },
// },
// },
// });

t.like(readLatest('published.wallet.agoric1buyer'), {
status: {
id: `${collateralBrandKey}-bid2`,
payouts: likePayouts(outcome.bids[1].payouts),
},
});

console.log(collateralBrandKey, 'step 5 of 10');
await advanceTimeBy(3, 'minutes');

console.log(collateralBrandKey, 'step 6 of 10');
await advanceTimeBy(3, 'minutes');
t.like(readLatest(`published.auction.book${managerIndex}`), {
collateralAvailable: { value: 9659301n },
});

console.log(collateralBrandKey, 'step 7 of 10');
await advanceTimeBy(3, 'minutes');

console.log(collateralBrandKey, 'step 8 of 10');
await advanceTimeBy(3, 'minutes');

console.log(collateralBrandKey, 'step 9 of 10');
await advanceTimeBy(3, 'minutes');
// Not part of product spec
t.like(readLatest(metricsPath), {
numActiveVaults: 0,
numLiquidationsCompleted: setup.vaults.length,
numLiquidatingVaults: 0,
retainedCollateral: { value: 0n },
totalCollateral: { value: 0n },
totalCollateralSold: { value: 35340699n },
totalDebt: { value: 0n },
totalOverageReceived: { value: 0n },
totalProceedsReceived: { value: 309540000n },
totalShortfallReceived: { value: 0n },
});

console.log(collateralBrandKey, 'step 10 of 10');
// continuing after now would start a new auction
{
const { nextDescendingStepTime, nextStartTime } = readLatest(
'published.auction.schedule',
) as Record<string, import('@agoric/time').TimestampRecord>;
t.is(nextDescendingStepTime.absValue, nextStartTime.absValue);
}

// bid3 still live because it's not fully satisfied
const { liveOffers } = readLatest('published.wallet.agoric1buyer.current');
t.is(liveOffers[0][1].id, `${collateralBrandKey}-bid3`);

// restart Zoe
// /////// Upgrading ////////////////////////////////
await buildAndExecuteProposal(
'@agoric/builders/scripts/vats/upgrade-zoe-proposal.js',
);

await buyer.tryExitOffer(`${collateralBrandKey}-bid3`);
t.like(readLatest('published.wallet.agoric1buyer'), {
status: {
id: `${collateralBrandKey}-bid3`,
payouts: likePayouts(outcome.bids[2].payouts),
},
});

// TODO express spec up top in a way it can be passed in here
// check.vaultNotification(managerIndex, 0, {
// debt: undefined,
// vaultState: 'liquidated',
// locked: {
// value: scale6(outcome.vaultsActual[0].locked),
// },
// });
// check.vaultNotification(managerIndex, 1, {
// debt: undefined,
// vaultState: 'liquidated',
// locked: {
// value: scale6(outcome.vaultsActual[1].locked),
// },
// });
}

// // check reserve balances
// t.like(readLatest('published.reserve.metrics'), {
// allocations: {
// [collateralBrandKey]: {
// value: scale6(outcome.reserve.allocations[collateralBrandKey]),
// },
// },
// shortfallBalance: { value: scale6(outcome.reserve.shortfall) },
// });
};

test.serial.failing(
'wallet survives zoe null upgrade',
checkFlow1,
{ collateralBrandKey: 'ATOM', managerIndex: 0 },
{},
);
8 changes: 6 additions & 2 deletions packages/zoe/src/contractFacet/zcfZygote.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export const makeZCFZygote = async (
instanceRecHolder = makeInstanceRecord(instanceRecordFromZoe);
instantiateIssuerStorage(issuerStorageFromZoe);
zcfBaggage.init('instanceRecHolder', instanceRecHolder);
zcfBaggage.init('repairedContractCompletionWatcher', true);

const startFn = start || prepare;
if (privateArgsShape) {
Expand Down Expand Up @@ -423,8 +424,11 @@ export const makeZCFZygote = async (
instanceRecHolder = zcfBaggage.get('instanceRecHolder');
initSeatMgrAndMintKind();

if (privateArgsShape) {
mustMatch(privateArgs, privateArgsShape, 'privateArgs');
await null;
if (!zcfBaggage.has('repairedContractCompletionWatcher')) {
await E(zoeInstanceAdmin).repairContractCompletionWatcher();
console.log(`Repaired contract completion watcher`);
zcfBaggage.init('repairedContractCompletionWatcher', true);
}

// restart an upgradeable contract
Expand Down
1 change: 1 addition & 0 deletions packages/zoe/src/internal-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
* @property {(strings: Array<string>) => void} setOfferFilter
* @property {() => Array<string>} getOfferFilter
* @property {(seatHandle: SeatHandle) => Subscriber<AmountKeywordRecord>} getExitSubscriber
* @property {() => void} repairContractCompletionWatcher
*/

/**
Expand Down
1 change: 1 addition & 0 deletions packages/zoe/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export const InstanceAdminI = M.interface('InstanceAdmin', {
getOfferFilter: M.call().returns(M.arrayOf(M.string())),
getExitSubscriber: M.call(SeatShape).returns(SubscriberShape),
isBlocked: M.call(M.string()).returns(M.boolean()),
repairContractCompletionWatcher: M.call().returns(),
});

export const InstanceStorageManagerIKit = harden({
Expand Down
Loading

0 comments on commit d141183

Please sign in to comment.