Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: delay addition of issuer to auctioneer until a good time #8398

Merged
merged 5 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions packages/boot/test/bootstrapTests/test-addAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';

import type { TestFn } from 'ava';

import { TimeMath } from '@agoric/time';
import {
LiquidationTestContext,
makeLiquidationTestContext,
} from './liquidation.ts';
import { makeProposalExtractor } from './supports.ts';

const test = anyTest as TestFn<
LiquidationTestContext & {
getCollateralProposal: (
name: string,
id: string,
) => Awaited<ReturnType<ReturnType<typeof makeProposalExtractor>>>;
}
>;

const auctioneerPath = 'published.auction';

test.before(async t => {
const context = await makeLiquidationTestContext(t);
const proposal = await context.buildProposal({
package: 'builders',
packageScriptName: 'build:add-STARS-proposal',
});

t.log('installing proposal');
// share a single proposal so tests don't stomp on each other's files; It has
// to be edited by each so as not to re-use keywords.
for await (const bundle of proposal.bundles) {
await context.controller.validateAndInstallBundle(bundle);
}
t.log('installed', proposal.bundles.length, 'bundles');

const getCollateralProposal = (name, id) => {
// stringify, modify and parse because modifying a deep copy was fragile.
const proposalJSON = JSON.stringify(proposal);
const proposalMod = proposalJSON
.replaceAll('STARS', name)
.replaceAll('ibc/987C17B1', `ibc/987C17B1${id}`);
return JSON.parse(proposalMod);
};
t.context = {
...context,
getCollateralProposal,
};
t.log('installed', proposal.bundles.length, 'bundles');
});

test.after.always(t => {
// This will fail if a subset of tests are run. It detects that three
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this check? Is each test not independent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each test now ends by verifying that readLatest(${auctioneerPath}.book1) finds something. What's actually happening is that the tests will sequentially add book1, book2, and book3. In order to allow for parallelism, since they take so long, I didn't make them sequential, so I don't know what order they run in.

I could have each test check all three potentially-present nodes and verify that one has a matching name. I thought it more straightforward to verify in the .after that 3 had been added. Please let me know if you think there's a more legible approach.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now that I moved building the proposal into .before, the total time was 38s. When I modified the tests to .serial that grew to 40s, which is probably not significant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not significant. also probably a smaller difference in CI with fewer cores.

// collaterals were added to the auction after ATOM.
t.like(t.context.readLatest(`${auctioneerPath}.book3`), {
currentPriceLevel: null,
});
return t.context.shutdown && t.context.shutdown();
});

test('addAsset to quiescent auction', async t => {
const { advanceTimeTo, readLatest } = t.context;

const proposal = t.context.getCollateralProposal('COMETS', 'A');
const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.evals,
};

const { EV } = t.context.runUtils;

const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit');
const schedules = await EV(auctioneerKit.creatorFacet).getSchedule();
const { liveAuctionSchedule, nextAuctionSchedule } = schedules;
const nextEndTime = liveAuctionSchedule
? liveAuctionSchedule.endTime
: nextAuctionSchedule.endTime;
const fiveMinutes = harden({
relValue: 5n * 60n,
timerBrand: nextEndTime.timerBrand,
});
const nextQuiescentTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes);
await advanceTimeTo(nextQuiescentTime);

const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem(
'coreEvalBridgeHandler',
);
await EV(coreEvalBridgeHandler).fromBridge(bridgeMessage);
t.log('proposal executed');

t.like(readLatest(`${auctioneerPath}.book1`), {
currentPriceLevel: null,
});
});

test('addAsset to active auction', async t => {
const { advanceTimeTo, readLatest } = t.context;
const { EV } = t.context.runUtils;

t.like(readLatest(`${auctioneerPath}.book0`), { startPrice: null });

const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit');
const schedules = await EV(auctioneerKit.creatorFacet).getSchedule();
const { nextAuctionSchedule } = schedules;
t.truthy(nextAuctionSchedule);
const nextStartTime = nextAuctionSchedule.startTime;
const fiveMinutes = harden({
relValue: 5n * 60n,
timerBrand: nextStartTime.timerBrand,
});
const futureBusyTime = TimeMath.addAbsRel(nextStartTime, fiveMinutes);

await advanceTimeTo(futureBusyTime);

t.log('launching proposal');

const proposal = t.context.getCollateralProposal('PLANETS', 'B');
const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.evals,
};
t.log({ bridgeMessage });

const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem(
'coreEvalBridgeHandler',
);
EV(coreEvalBridgeHandler).fromBridge(bridgeMessage);

const nextEndTime = nextAuctionSchedule.endTime;
const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes);
await advanceTimeTo(afterEndTime);
t.log('proposal executed');

const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule();
// TimeMath.compareAbs() can't handle brands processed by kmarshall
t.truthy(
schedules.nextAuctionSchedule.endTime.absValue <
schedulesAfter.nextAuctionSchedule.endTime.absValue,
);

t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null });
});

test('addAsset to auction starting soon', async t => {
const { advanceTimeTo, readLatest } = t.context;
const { EV } = t.context.runUtils;

const auctioneerKit = await EV.vat('bootstrap').consumeItem('auctioneerKit');
const schedules = await EV(auctioneerKit.creatorFacet).getSchedule();
const { nextAuctionSchedule } = schedules;
t.truthy(nextAuctionSchedule);
const nextStartTime = nextAuctionSchedule.startTime;
const fiveMinutes = harden({
relValue: 5n * 60n,
timerBrand: nextStartTime.timerBrand,
});
const tooCloseTime = TimeMath.subtractAbsRel(nextStartTime, fiveMinutes);

await advanceTimeTo(tooCloseTime);

const proposal = t.context.getCollateralProposal('MOONS', 'C');
t.log('launching proposal');
const bridgeMessage = {
type: 'CORE_EVAL',
evals: proposal.evals,
};
t.log({ bridgeMessage });

const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem(
'coreEvalBridgeHandler',
);
EV(coreEvalBridgeHandler).fromBridge(bridgeMessage);

const nextEndTime = nextAuctionSchedule.endTime;
const afterEndTime = TimeMath.addAbsRel(nextEndTime, fiveMinutes);
await advanceTimeTo(afterEndTime);

t.log('proposal executed');

const schedulesAfter = await EV(auctioneerKit.creatorFacet).getSchedule();
t.truthy(
schedules.nextAuctionSchedule.endTime.absValue <
schedulesAfter.nextAuctionSchedule.endTime.absValue,
);
t.like(readLatest(`${auctioneerPath}.book1`), { currentPriceLevel: null });
});
1 change: 1 addition & 0 deletions packages/inter-protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@endo/far": "^0.2.21",
"@endo/marshal": "^0.8.8",
"@endo/nat": "^4.1.30",
"@endo/promise-kit": "^0.2.59",
"jessie.js": "^0.3.2"
},
"devDependencies": {
Expand Down
105 changes: 100 additions & 5 deletions packages/inter-protocol/src/proposals/addAssetToVault.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
// @jessie-check
// @ts-check

import { ToFarFunction } from '@endo/captp';
import { Far } from '@endo/marshal';
import { AmountMath, AssetKind } from '@agoric/ertp';
import { deeplyFulfilledObject } from '@agoric/internal';
import { makeRatio } from '@agoric/zoe/src/contractSupport/index.js';
import { parseRatio } from '@agoric/zoe/src/contractSupport/ratio.js';
import { E } from '@endo/far';
import { Stable } from '@agoric/internal/src/tokens.js';
import { TimeMath } from '@agoric/time/src/timeMath.js';
import { makePromiseKit } from '@endo/promise-kit';

import { instanceNameFor } from './price-feed-proposal.js';
import { reserveThenGetNames } from './utils.js';

export * from './startPSM.js';

const { quote: q } = assert;

/**
* @typedef {object} InterchainAssetOptions
* @property {string} [issuerBoardId]
Expand All @@ -24,8 +32,10 @@ export * from './startPSM.js';
* @property {number} [initialPrice]
*/

/** @typedef {import('./econ-behaviors.js').EconomyBootstrapPowers} EconomyBootstrapPowers */

/**
* @param {EconomyBootstrapPowers} powers
* @param {BootstrapPowers} powers
* @param {object} config
* @param {object} config.options
* @param {InterchainAssetOptions} config.options.interchainAssetOptions
Expand Down Expand Up @@ -216,7 +226,74 @@ export const registerScaledPriceAuthority = async (
);
};

/** @typedef {import('./econ-behaviors.js').EconomyBootstrapPowers} EconomyBootstrapPowers */
// wait a short while after end to allow things to settle
turadg marked this conversation as resolved.
Show resolved Hide resolved
const BUFFER = 5n * 60n;
// let's insist on 20 minutes leeway for running the scripts
const COMPLETION = 20n * 60n;

/**
* This function works around an issue identified in #8307 and #8296, and fixed
* in #8301. The fix is needed until #8301 makes it into production.
*
* If there is a liveSchedule, 1) run now if start is far enough away,
* otherwise, 2) run after endTime. If neither liveSchedule nor nextSchedule is
* defined, 3) run now. If there is only a nextSchedule, 4) run now if startTime
* is far enough away, else 5) run after endTime
*
* @param {import('../auction/scheduler.js').FullSchedule} schedules
* @param {ERef<import('@agoric/time/src/types').TimerService>} timer
* @param {() => void} thunk
*/
const whenQuiescent = async (schedules, timer, thunk) => {
turadg marked this conversation as resolved.
Show resolved Hide resolved
const { nextAuctionSchedule, liveAuctionSchedule } = schedules;
const now = await E(timer).getCurrentTimestamp();

const waker = Far('addAssetWaker', { wake: () => thunk() });

if (liveAuctionSchedule) {
const safeStart = TimeMath.subtractAbsRel(
liveAuctionSchedule.startTime,
COMPLETION,
);

if (TimeMath.compareAbs(safeStart, now) < 0) {
// case 2
turadg marked this conversation as resolved.
Show resolved Hide resolved
console.warn(
`Add Asset after live schedule's endtime: ${q(
liveAuctionSchedule.endTime,
)}`,
);

return E(timer).setWakeup(
TimeMath.addAbsRel(liveAuctionSchedule.endTime, BUFFER),
waker,
);
}
}

if (!liveAuctionSchedule && nextAuctionSchedule) {
const safeStart = TimeMath.subtractAbsRel(
nextAuctionSchedule.startTime,
COMPLETION,
);
if (TimeMath.compareAbs(safeStart, now) < 0) {
// case 5
console.warn(
`Add Asset after next schedule's endtime: ${q(
nextAuctionSchedule.endTime,
)}`,
);
return E(timer).setWakeup(
TimeMath.addAbsRel(nextAuctionSchedule.endTime, BUFFER),
waker,
);
}
}

// cases 1, 3, and 4 fall through to here.
console.warn(`Add Asset immediately`, thunk);
return thunk();
};

/**
* @param {EconomyBootstrapPowers} powers
Expand All @@ -228,7 +305,12 @@ export const registerScaledPriceAuthority = async (
*/
export const addAssetToVault = async (
{
consume: { vaultFactoryKit, agoricNamesAdmin, auctioneerKit },
consume: {
vaultFactoryKit,
agoricNamesAdmin,
auctioneerKit,
chainTimerService,
},
brand: {
consume: { [Stable.symbol]: stableP },
},
Expand Down Expand Up @@ -263,6 +345,20 @@ export const addAssetToVault = async (
// eslint-disable-next-line no-restricted-syntax -- allow this computed property
await consumeInstance[oracleInstanceName];

const auctioneerCreator = E.get(auctioneerKit).creatorFacet;
const schedules = await E(auctioneerCreator).getSchedule();

const finishPromiseKit = makePromiseKit();
const addBrandThenResolve = ToFarFunction('addBrandThenResolve', async () => {
await E(auctioneerCreator).addBrand(interchainIssuer, keyword);
finishPromiseKit.resolve(undefined);
});

// schedules actions on a timer (or does it immediately).
// finishPromiseKit signals completion.
void whenQuiescent(schedules, chainTimerService, addBrandThenResolve);
await finishPromiseKit.promise;

const stable = await stableP;
const vaultFactoryCreator = E.get(vaultFactoryKit).creatorFacet;
await E(vaultFactoryCreator).addVaultType(interchainIssuer, keyword, {
Expand All @@ -277,8 +373,6 @@ export const addAssetToVault = async (
mintFee: makeRatio(50n, stable, 10_000n),
liquidationPenalty: makeRatio(1n, stable),
});
const auctioneerCreator = E.get(auctioneerKit).creatorFacet;
await E(auctioneerCreator).addBrand(interchainIssuer, keyword);
};

export const getManifestForAddAssetToVault = (
Expand Down Expand Up @@ -338,6 +432,7 @@ export const getManifestForAddAssetToVault = (
auctioneerKit: 'auctioneer',
vaultFactoryKit: 'vaultFactory',
agoricNamesAdmin: true,
chainTimerService: true,
},
brand: {
consume: { [Stable.symbol]: true },
Expand Down
Loading