From b446016022db7caf2e240dbcd12686c844ad65f5 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 20 Jun 2024 19:26:07 -0400 Subject: [PATCH 1/3] feat(orchestrator): membrane-friendly timerUtils --- .../src/examples/stakeBld.contract.js | 2 + .../src/exos/cosmos-orchestration-account.js | 7 +- .../src/exos/local-orchestration-account.js | 17 ++-- .../orchestration/src/exos/time-helper.js | 77 +++++++++++++++++++ packages/orchestration/src/utils/cosmos.js | 9 +++ .../orchestration/src/utils/start-helper.js | 3 + packages/orchestration/src/utils/time.js | 63 --------------- .../local-orchestration-account-kit.test.ts | 8 +- .../time.test.ts => exos/time-helper.test.ts} | 34 ++++---- .../orchestration/test/utils/cosmos.test.ts | 10 +++ 10 files changed, 136 insertions(+), 94 deletions(-) create mode 100644 packages/orchestration/src/exos/time-helper.js delete mode 100644 packages/orchestration/src/utils/time.js rename packages/orchestration/test/{utils/time.test.ts => exos/time-helper.test.ts} (55%) create mode 100644 packages/orchestration/test/utils/cosmos.test.ts diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 2339ef2175b..3bdf6ce9241 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -11,6 +11,7 @@ import { deeplyFulfilled } from '@endo/marshal'; import { M } from '@endo/patterns'; import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js'; import { makeChainHub } from '../exos/chain-hub.js'; +import { makeTimeHelper } from '../exos/time-helper.js'; /** * @import {NameHub} from '@agoric/vats'; @@ -48,6 +49,7 @@ export const start = async (zcf, privateArgs, baggage) => { privateArgs.timerService, vowTools, makeChainHub(privateArgs.agoricNames), + makeTimeHelper(privateArgs.timerService), ); // ---------------- diff --git a/packages/orchestration/src/exos/cosmos-orchestration-account.js b/packages/orchestration/src/exos/cosmos-orchestration-account.js index 46ddd1e6bcb..b142a684b08 100644 --- a/packages/orchestration/src/exos/cosmos-orchestration-account.js +++ b/packages/orchestration/src/exos/cosmos-orchestration-account.js @@ -28,9 +28,12 @@ import { CoinShape, DelegationShape, } from '../typeGuards.js'; -import { maxClockSkew, tryDecodeResponse } from '../utils/cosmos.js'; +import { + maxClockSkew, + tryDecodeResponse, + dateInSeconds, +} from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; -import { dateInSeconds } from '../utils/time.js'; /** * @import {AmountArg, IcaAccount, ChainAddress, CosmosValidatorAddress, ICQConnection, StakingAccountActions, DenomAmount, OrchestrationAccountI, DenomArg} from '../types.js'; diff --git a/packages/orchestration/src/exos/local-orchestration-account.js b/packages/orchestration/src/exos/local-orchestration-account.js index c183bbd06d8..c333ca72698 100644 --- a/packages/orchestration/src/exos/local-orchestration-account.js +++ b/packages/orchestration/src/exos/local-orchestration-account.js @@ -12,9 +12,8 @@ import { ChainAmountShape, IBCTransferOptionsShape, } from '../typeGuards.js'; -import { maxClockSkew } from '../utils/cosmos.js'; +import { maxClockSkew, dateInSeconds } from '../utils/cosmos.js'; import { orchestrationAccountMethods } from '../utils/orchestrationAccount.js'; -import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; /** * @import {LocalChainAccount} from '@agoric/vats/src/localchain.js'; @@ -26,6 +25,7 @@ import { dateInSeconds, makeTimestampHelper } from '../utils/time.js'; * @import {PromiseVow, VowTools} from '@agoric/vow'; * @import {TypedJson} from '@agoric/cosmic-proto'; * @import {ChainHub} from './chain-hub.js'; + * @import {TimeHelper} from './time-helper.js'; */ const trace = makeTracer('LOA'); @@ -65,6 +65,7 @@ const PUBLIC_TOPICS = { * @param {Remote} timerService * @param {VowTools} vowTools * @param {ChainHub} chainHub + * @param {TimeHelper} timeHelper */ export const prepareLocalOrchestrationAccountKit = ( zone, @@ -73,11 +74,10 @@ export const prepareLocalOrchestrationAccountKit = ( timerService, { watch, when, allVows }, chainHub, -) => { - const timestampHelper = makeTimestampHelper(timerService); - + timeHelper, +) => /** Make an object wrapping an LCA with Zoe interfaces. */ - const makeLocalOrchestrationAccountKit = zone.exoClassKit( + zone.exoClassKit( 'Local Orchestration Account Kit', { holder: HolderI, @@ -402,7 +402,7 @@ export const prepareLocalOrchestrationAccountKit = ( // TODO #9324 what's a reasonable default? currently 5 minutes // FIXME: do not call `getTimeoutTimestampNS` if `opts.timeoutTimestamp` or `opts.timeoutHeight` is provided const timeoutTimestampV = watch( - timestampHelper.getTimeoutTimestampNS(), + timeHelper.getTimeoutTimestampNS(), this.facets.getTimeoutTimestampWatcher, { opts }, ); @@ -422,7 +422,6 @@ export const prepareLocalOrchestrationAccountKit = ( }, }, ); - return makeLocalOrchestrationAccountKit; -}; + /** @typedef {ReturnType} MakeLocalOrchestrationAccountKit */ /** @typedef {ReturnType} LocalOrchestrationAccountKit */ diff --git a/packages/orchestration/src/exos/time-helper.js b/packages/orchestration/src/exos/time-helper.js new file mode 100644 index 00000000000..e2bb3694138 --- /dev/null +++ b/packages/orchestration/src/exos/time-helper.js @@ -0,0 +1,77 @@ +import { RelativeTimeRecordShape, TimeMath } from '@agoric/time'; +import { VowShape } from '@agoric/vow'; +import { watch, allVows } from '@agoric/vow/vat.js'; +import { makeHeapZone } from '@agoric/zone'; +import { E } from '@endo/far'; +import { M } from '@endo/patterns'; + +/** + * @import {Remote} from '@agoric/internal';* + * @import {RelativeTimeRecord, TimerBrand, TimerService, TimestampRecord} from '@agoric/time'; + * @import {Vow} from '@agoric/vow'; + * @import {Zone} from '@agoric/zone'; + */ + +export const SECONDS_PER_MINUTE = 60n; +export const NANOSECONDS_PER_SECOND = 1_000_000_000n; + +/** + * @param {Remote} timerService + * @param {Zone} [zone] + */ + +export const makeTimeHelper = (timerService, zone = makeHeapZone()) => { + /** @type {TimerBrand | undefined} */ + let brandCache; + const getBrand = () => { + if (brandCache) return brandCache; + return watch(E(timerService).getTimerBrand(), { + onFulfilled: timerBrand => { + brandCache = timerBrand; + return timerBrand; + }, + }); + }; + + return zone.exo( + 'Time Helper', + M.interface('TimeHelperI', { + getTimeoutTimestampNS: M.call() + .optional(RelativeTimeRecordShape) + .returns(VowShape), + }), + { + /** + * Takes the current time from ChainTimerService and adds a relative time + * to determine a timeout timestamp in nanoseconds. Useful for + * {@link MsgTransfer.timeoutTimestamp}. + * + * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes + * @returns {Vow} Timeout timestamp in absolute nanoseconds since + * unix epoch + */ + getTimeoutTimestampNS(relativeTime) { + return watch( + allVows([E(timerService).getCurrentTimestamp(), getBrand()]), + { + /** @param {[TimestampRecord, TimerBrand]} results */ + onFulfilled([currentTime, timerBrand]) { + const timeout = + relativeTime || + TimeMath.coerceRelativeTimeRecord( + SECONDS_PER_MINUTE * 5n, + timerBrand, + ); + return ( + TimeMath.addAbsRel(currentTime, timeout).absValue * + NANOSECONDS_PER_SECOND + ); + }, + }, + ); + }, + }, + ); +}; + +/** @typedef {Awaited>} TimeHelper */ diff --git a/packages/orchestration/src/utils/cosmos.js b/packages/orchestration/src/utils/cosmos.js index d84a2e5f033..22ae769b1c1 100644 --- a/packages/orchestration/src/utils/cosmos.js +++ b/packages/orchestration/src/utils/cosmos.js @@ -34,3 +34,12 @@ export const tryDecodeResponse = (ackStr, fromProtoMsg) => { throw assert.error(`bad response: ${ackStr}`, undefined, { cause }); } }; + +/** + * Convert a Date from a Cosmos message, which has millisecond precision, to a + * BigInt for number of seconds since epoch, for use in a timer. + * + * @param {Date} date + * @returns {bigint} + */ +export const dateInSeconds = date => BigInt(Math.floor(date.getTime() / 1000)); diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index d1e975dd198..34e23a35226 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -8,6 +8,7 @@ import { makeChainHub } from '../exos/chain-hub.js'; import { prepareRemoteChainFacade } from '../exos/remote-chain-facade.js'; import { prepareCosmosOrchestrationAccount } from '../exos/cosmos-orchestration-account.js'; import { prepareLocalChainFacade } from '../exos/local-chain-facade.js'; +import { makeTimeHelper } from '../exos/time-helper.js'; /** * @import {PromiseKit} from '@endo/promise-kit' @@ -48,6 +49,7 @@ export const provideOrchestration = ( const { agoricNames, timerService } = remotePowers; const chainHub = makeChainHub(agoricNames); + const timeHelper = makeTimeHelper(timerService); const vowTools = prepareVowTools(zone.subZone('vows')); @@ -59,6 +61,7 @@ export const provideOrchestration = ( timerService, vowTools, chainHub, + timeHelper, ); const asyncFlowTools = prepareAsyncFlowTools(zone.subZone('asyncFlow'), { diff --git a/packages/orchestration/src/utils/time.js b/packages/orchestration/src/utils/time.js deleted file mode 100644 index 8e8dc062f6e..00000000000 --- a/packages/orchestration/src/utils/time.js +++ /dev/null @@ -1,63 +0,0 @@ -import { E } from '@endo/far'; -import { TimeMath } from '@agoric/time'; - -/** - * @import {RelativeTimeRecord, TimerBrand, TimerService} from '@agoric/time'; - * @import {Remote} from '@agoric/internal'; - */ - -export const SECONDS_PER_MINUTE = 60n; -export const NANOSECONDS_PER_SECOND = 1_000_000_000n; - -/** - * XXX should this be durable? resumable? - * - * @param {Remote} timer - */ -export function makeTimestampHelper(timer) { - /** @type {TimerBrand | undefined} */ - let brandCache; - const getBrand = async () => { - if (brandCache) return brandCache; - brandCache = await E(timer).getTimerBrand(); - return brandCache; - }; - - return harden({ - /** - * XXX do this need to be resumable / use Vows? - * - * Takes the current time from ChainTimerService and adds a relative time to - * determine a timeout timestamp in nanoseconds. Useful for - * {@link MsgTransfer.timeoutTimestamp}. - * - * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes - * @returns {Promise} Timeout timestamp in absolute nanoseconds - * since unix epoch - */ - async getTimeoutTimestampNS(relativeTime) { - const currentTime = await E(timer).getCurrentTimestamp(); - const timeout = - relativeTime || - TimeMath.coerceRelativeTimeRecord( - SECONDS_PER_MINUTE * 5n, - await getBrand(), - ); - return ( - TimeMath.addAbsRel(currentTime, timeout).absValue * - NANOSECONDS_PER_SECOND - ); - }, - }); -} - -/** @typedef {Awaited>} TimestampHelper */ - -/** - * Convert a Date from a Cosmos message, which has millisecond precision, to a - * BigInt for number of seconds since epoch, for use in a timer. - * - * @param {Date} date - * @returns {bigint} - */ -export const dateInSeconds = date => BigInt(Math.floor(date.getTime() / 1000)); diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index 9a5533a0235..76da9316d6c 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -8,7 +8,10 @@ import { Far } from '@endo/far'; import { prepareLocalOrchestrationAccountKit } from '../../src/exos/local-orchestration-account.js'; import { ChainAddress } from '../../src/orchestration-api.js'; import { makeChainHub } from '../../src/exos/chain-hub.js'; -import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; +import { + makeTimeHelper, + NANOSECONDS_PER_SECOND, +} from '../../src/exos/time-helper.js'; import { commonSetup } from '../supports.js'; test('deposit, withdraw', async t => { @@ -34,6 +37,7 @@ test('deposit, withdraw', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), + makeTimeHelper(timer), ); t.log('request account from vat-localchain'); @@ -105,6 +109,7 @@ test('delegate, undelegate', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), + makeTimeHelper(timer), ); t.log('request account from vat-localchain'); @@ -157,6 +162,7 @@ test('transfer', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), + makeTimeHelper(timer), ); t.log('request account from vat-localchain'); diff --git a/packages/orchestration/test/utils/time.test.ts b/packages/orchestration/test/exos/time-helper.test.ts similarity index 55% rename from packages/orchestration/test/utils/time.test.ts rename to packages/orchestration/test/exos/time-helper.test.ts index 01f496814c3..e761b5ce3ed 100644 --- a/packages/orchestration/test/utils/time.test.ts +++ b/packages/orchestration/test/exos/time-helper.test.ts @@ -1,29 +1,31 @@ +/* eslint-disable @jessie.js/safe-await-separator */ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { V } from '@agoric/vow/vat.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { TimeMath } from '@agoric/time'; import { - dateInSeconds, - makeTimestampHelper, + makeTimeHelper, NANOSECONDS_PER_SECOND, SECONDS_PER_MINUTE, -} from '../../src/utils/time.js'; +} from '../../src/exos/time-helper.js'; -test('makeTimestampHelper - getCurrentTimestamp', async t => { +test('makeTimeHelper - getCurrentTimestamp', async t => { const timer = buildZoeManualTimer(t.log); const timerBrand = timer.getTimerBrand(); t.is(timer.getCurrentTimestamp().absValue, 0n, 'current time is 0n'); - const { getTimeoutTimestampNS } = makeTimestampHelper(timer); - await null; + const timeHelper = makeTimeHelper(timer); t.is( - await getTimeoutTimestampNS(), + await V.when(timeHelper.getTimeoutTimestampNS()), 5n * SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND, 'default timestamp is 5 minutes from current time, in nanoseconds', ); t.is( - await getTimeoutTimestampNS( - TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + await V.when( + timeHelper.getTimeoutTimestampNS( + TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + ), ), 1n * NANOSECONDS_PER_SECOND, 'timestamp is 1 second since unix epoch, in nanoseconds', @@ -32,18 +34,12 @@ test('makeTimestampHelper - getCurrentTimestamp', async t => { // advance timer by 3 seconds await timer.tickN(3); t.is( - await getTimeoutTimestampNS( - TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + await V.when( + timeHelper.getTimeoutTimestampNS( + TimeMath.coerceRelativeTimeRecord(1n, timerBrand), + ), ), (1n + 3n) * NANOSECONDS_PER_SECOND, 'timestamp is 4 seconds since unix epoch, in nanoseconds', ); }); - -test('dateInSeconds', t => { - t.is(dateInSeconds(new Date(1)), 0n); - t.is(dateInSeconds(new Date(999)), 0n); - t.is(dateInSeconds(new Date(1000)), 1n); - - t.is(dateInSeconds(new Date('2025-12-17T12:23:45Z')), 1765974225n); -}); diff --git a/packages/orchestration/test/utils/cosmos.test.ts b/packages/orchestration/test/utils/cosmos.test.ts new file mode 100644 index 00000000000..44647e40065 --- /dev/null +++ b/packages/orchestration/test/utils/cosmos.test.ts @@ -0,0 +1,10 @@ +import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; +import { dateInSeconds } from '../../src/utils/cosmos.js'; + +test('dateInSeconds', t => { + t.is(dateInSeconds(new Date(1)), 0n); + t.is(dateInSeconds(new Date(999)), 0n); + t.is(dateInSeconds(new Date(1000)), 1n); + + t.is(dateInSeconds(new Date('2025-12-17T12:23:45Z')), 1765974225n); +}); From 509a9c4cc726ca54bbc8696e98ca4d164ea8c826 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Thu, 20 Jun 2024 23:24:51 -0400 Subject: [PATCH 2/3] refactor(time-helper): optionally durable --- .../src/examples/stakeBld.contract.js | 17 ++--- .../orchestration/src/exos/time-helper.js | 63 +++++++++++-------- .../orchestration/src/utils/start-helper.js | 10 +-- .../local-orchestration-account-kit.test.ts | 7 ++- .../test/exos/time-helper.test.ts | 14 ++++- 5 files changed, 70 insertions(+), 41 deletions(-) diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 3bdf6ce9241..0fc4c16bd8d 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -7,6 +7,7 @@ import { withdrawFromSeat } from '@agoric/zoe/src/contractSupport/zoeHelpers.js' import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { prepareVowTools, heapVowE as E } from '@agoric/vow/vat.js'; +import { makeHeapZone } from '@agoric/zone'; import { deeplyFulfilled } from '@endo/marshal'; import { M } from '@endo/patterns'; import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js'; @@ -35,21 +36,23 @@ const trace = makeTracer('StakeBld'); */ export const start = async (zcf, privateArgs, baggage) => { const zone = makeDurableZone(baggage); + const { agoricNames, marshaller, timerService } = privateArgs; - const { makeRecorderKit } = prepareRecorderKitMakers( - baggage, - privateArgs.marshaller, - ); + const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const vowTools = prepareVowTools(zone.subZone('vows')); + const timeHelper = makeTimeHelper(makeHeapZone(), { + timerService, + vowTools, + }); const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit( zone, makeRecorderKit, zcf, - privateArgs.timerService, + timerService, vowTools, - makeChainHub(privateArgs.agoricNames), - makeTimeHelper(privateArgs.timerService), + makeChainHub(agoricNames), + timeHelper, ); // ---------------- diff --git a/packages/orchestration/src/exos/time-helper.js b/packages/orchestration/src/exos/time-helper.js index e2bb3694138..9786af5f597 100644 --- a/packages/orchestration/src/exos/time-helper.js +++ b/packages/orchestration/src/exos/time-helper.js @@ -1,14 +1,12 @@ import { RelativeTimeRecordShape, TimeMath } from '@agoric/time'; import { VowShape } from '@agoric/vow'; -import { watch, allVows } from '@agoric/vow/vat.js'; -import { makeHeapZone } from '@agoric/zone'; -import { E } from '@endo/far'; +import { E, Far } from '@endo/far'; import { M } from '@endo/patterns'; /** * @import {Remote} from '@agoric/internal';* * @import {RelativeTimeRecord, TimerBrand, TimerService, TimestampRecord} from '@agoric/time'; - * @import {Vow} from '@agoric/vow'; + * @import {Vow, VowTools} from '@agoric/vow'; * @import {Zone} from '@agoric/zone'; */ @@ -16,31 +14,41 @@ export const SECONDS_PER_MINUTE = 60n; export const NANOSECONDS_PER_SECOND = 1_000_000_000n; /** - * @param {Remote} timerService - * @param {Zone} [zone] + * @param {Zone} zone + * @param {{ timerService: Remote; vowTools: VowTools }} powers */ - -export const makeTimeHelper = (timerService, zone = makeHeapZone()) => { - /** @type {TimerBrand | undefined} */ - let brandCache; - const getBrand = () => { - if (brandCache) return brandCache; - return watch(E(timerService).getTimerBrand(), { - onFulfilled: timerBrand => { - brandCache = timerBrand; - return timerBrand; - }, - }); - }; - - return zone.exo( +export const makeTimeHelper = ( + zone, + { timerService, vowTools: { watch, allVows } }, +) => { + const timeHelper = zone.exoClass( 'Time Helper', M.interface('TimeHelperI', { getTimeoutTimestampNS: M.call() .optional(RelativeTimeRecordShape) .returns(VowShape), + getBrand: M.call().returns(VowShape), }), + () => + /** @type {{ brandCache: TimerBrand | undefined }} */ ({ + brandCache: undefined, + }), { + /** @returns {Vow} */ + getBrand() { + // XXX this is a common use case that should have a helper like `provideSingleton` + if (this.state.brandCache) return watch(this.state.brandCache); + return watch( + E(timerService).getTimerBrand(), + Far('BrandWatcher', { + /** @param {TimerBrand} timerBrand */ + onFulfilled: timerBrand => { + this.state.brandCache = timerBrand; + return timerBrand; + }, + }), + ); + }, /** * Takes the current time from ChainTimerService and adds a relative time * to determine a timeout timestamp in nanoseconds. Useful for @@ -52,8 +60,11 @@ export const makeTimeHelper = (timerService, zone = makeHeapZone()) => { */ getTimeoutTimestampNS(relativeTime) { return watch( - allVows([E(timerService).getCurrentTimestamp(), getBrand()]), - { + allVows([ + E(timerService).getCurrentTimestamp(), + this.self.getBrand(), + ]), + Far('TimestampWatcher', { /** @param {[TimestampRecord, TimerBrand]} results */ onFulfilled([currentTime, timerBrand]) { const timeout = @@ -67,11 +78,13 @@ export const makeTimeHelper = (timerService, zone = makeHeapZone()) => { NANOSECONDS_PER_SECOND ); }, - }, + }), ); }, }, ); + return timeHelper(); }; +harden(makeTimeHelper); -/** @typedef {Awaited>} TimeHelper */ +/** @typedef {ReturnType} TimeHelper */ diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 34e23a35226..1254c6ce731 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -2,6 +2,7 @@ import { prepareAsyncFlowTools } from '@agoric/async-flow'; import { prepareVowTools } from '@agoric/vow'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; +import { makeHeapZone } from '@agoric/zone'; import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js'; import { makeOrchestrationFacade } from '../facade.js'; import { makeChainHub } from '../exos/chain-hub.js'; @@ -46,12 +47,13 @@ export const provideOrchestration = ( marshaller, ) => { const zone = makeDurableZone(baggage); + const vowTools = prepareVowTools(zone.subZone('vows')); const { agoricNames, timerService } = remotePowers; - const chainHub = makeChainHub(agoricNames); - const timeHelper = makeTimeHelper(timerService); - - const vowTools = prepareVowTools(zone.subZone('vows')); + const timeHelper = makeTimeHelper(makeHeapZone(), { + timerService, + vowTools, + }); const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const makeLocalOrchestrationAccountKit = prepareLocalOrchestrationAccountKit( diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index 76da9316d6c..8660dc5aa97 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -4,6 +4,7 @@ import { AmountMath } from '@agoric/ertp'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { heapVowE as E } from '@agoric/vow/vat.js'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; +import { makeHeapZone } from '@agoric/zone'; import { Far } from '@endo/far'; import { prepareLocalOrchestrationAccountKit } from '../../src/exos/local-orchestration-account.js'; import { ChainAddress } from '../../src/orchestration-api.js'; @@ -37,7 +38,7 @@ test('deposit, withdraw', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), - makeTimeHelper(timer), + makeTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), ); t.log('request account from vat-localchain'); @@ -109,7 +110,7 @@ test('delegate, undelegate', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), - makeTimeHelper(timer), + makeTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), ); t.log('request account from vat-localchain'); @@ -162,7 +163,7 @@ test('transfer', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), - makeTimeHelper(timer), + makeTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), ); t.log('request account from vat-localchain'); diff --git a/packages/orchestration/test/exos/time-helper.test.ts b/packages/orchestration/test/exos/time-helper.test.ts index e761b5ce3ed..691cb054de9 100644 --- a/packages/orchestration/test/exos/time-helper.test.ts +++ b/packages/orchestration/test/exos/time-helper.test.ts @@ -1,8 +1,9 @@ /* eslint-disable @jessie.js/safe-await-separator */ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { V } from '@agoric/vow/vat.js'; +import { prepareVowTools, V } from '@agoric/vow/vat.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { TimeMath } from '@agoric/time'; +import { makeHeapZone } from '@agoric/zone'; import { makeTimeHelper, NANOSECONDS_PER_SECOND, @@ -11,15 +12,24 @@ import { test('makeTimeHelper - getCurrentTimestamp', async t => { const timer = buildZoeManualTimer(t.log); + const vowTools = prepareVowTools(makeHeapZone()); const timerBrand = timer.getTimerBrand(); t.is(timer.getCurrentTimestamp().absValue, 0n, 'current time is 0n'); - const timeHelper = makeTimeHelper(timer); + const timeHelper = makeTimeHelper(makeHeapZone(), { + timerService: timer, + vowTools, + }); t.is( await V.when(timeHelper.getTimeoutTimestampNS()), 5n * SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND, 'default timestamp is 5 minutes from current time, in nanoseconds', ); + t.is( + await V.when(timeHelper.getBrand()), + timerBrand, + 'brand retrieved cache equals timerBrand', + ); t.is( await V.when( From 4c68b4edf0cae212f981eabc846bec64b5b41110 Mon Sep 17 00:00:00 2001 From: 0xPatrick Date: Tue, 25 Jun 2024 12:39:30 -0400 Subject: [PATCH 3/3] fixup! refactor(time-helper): optionally durable --- .../src/examples/stakeBld.contract.js | 4 +- .../orchestration/src/exos/time-helper.js | 160 +++++++++++------- .../orchestration/src/utils/start-helper.js | 4 +- .../local-orchestration-account-kit.test.ts | 8 +- .../test/exos/time-helper.test.ts | 15 +- 5 files changed, 116 insertions(+), 75 deletions(-) diff --git a/packages/orchestration/src/examples/stakeBld.contract.js b/packages/orchestration/src/examples/stakeBld.contract.js index 0fc4c16bd8d..61643573e6b 100644 --- a/packages/orchestration/src/examples/stakeBld.contract.js +++ b/packages/orchestration/src/examples/stakeBld.contract.js @@ -12,7 +12,7 @@ import { deeplyFulfilled } from '@endo/marshal'; import { M } from '@endo/patterns'; import { prepareLocalOrchestrationAccountKit } from '../exos/local-orchestration-account.js'; import { makeChainHub } from '../exos/chain-hub.js'; -import { makeTimeHelper } from '../exos/time-helper.js'; +import { prepareTimeHelper } from '../exos/time-helper.js'; /** * @import {NameHub} from '@agoric/vats'; @@ -40,7 +40,7 @@ export const start = async (zcf, privateArgs, baggage) => { const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); const vowTools = prepareVowTools(zone.subZone('vows')); - const timeHelper = makeTimeHelper(makeHeapZone(), { + const timeHelper = prepareTimeHelper(makeHeapZone(), { timerService, vowTools, }); diff --git a/packages/orchestration/src/exos/time-helper.js b/packages/orchestration/src/exos/time-helper.js index 9786af5f597..978d614d116 100644 --- a/packages/orchestration/src/exos/time-helper.js +++ b/packages/orchestration/src/exos/time-helper.js @@ -1,6 +1,12 @@ -import { RelativeTimeRecordShape, TimeMath } from '@agoric/time'; +import { BrandShape } from '@agoric/ertp'; +import { + RelativeTimeRecordShape, + TimeMath, + TimestampRecordShape, +} from '@agoric/time'; +import { pickFacet } from '@agoric/vat-data'; import { VowShape } from '@agoric/vow'; -import { E, Far } from '@endo/far'; +import { E } from '@endo/far'; import { M } from '@endo/patterns'; /** @@ -13,78 +19,112 @@ import { M } from '@endo/patterns'; export const SECONDS_PER_MINUTE = 60n; export const NANOSECONDS_PER_SECOND = 1_000_000_000n; +/** @typedef {{ timerService: Remote; vowTools: VowTools }} TimeHelperPowers */ + /** * @param {Zone} zone - * @param {{ timerService: Remote; vowTools: VowTools }} powers + * @param {TimeHelperPowers} powers */ -export const makeTimeHelper = ( +const prepareTimeHelperKit = ( zone, { timerService, vowTools: { watch, allVows } }, -) => { - const timeHelper = zone.exoClass( +) => + zone.exoClassKit( 'Time Helper', - M.interface('TimeHelperI', { - getTimeoutTimestampNS: M.call() - .optional(RelativeTimeRecordShape) - .returns(VowShape), - getBrand: M.call().returns(VowShape), - }), + { + getBrandWatcher: M.interface('GetBrandWatcherI', { + onFulfilled: M.call(BrandShape) + .optional(M.arrayOf(M.undefined())) // does not need watcherContext + .returns(BrandShape), + }), + getTimestampWatcher: M.interface('GetBrandWatcherI', { + onFulfilled: M.call([TimestampRecordShape, BrandShape]) + .optional({ + relativeTime: M.or(RelativeTimeRecordShape, M.undefined()), + }) + .returns(M.bigint()), + }), + public: M.interface('TimeHelperI', { + getTimeoutTimestampNS: M.call() + .optional(RelativeTimeRecordShape) + .returns(VowShape), + getBrand: M.call().returns(VowShape), + }), + }, () => /** @type {{ brandCache: TimerBrand | undefined }} */ ({ brandCache: undefined, }), { - /** @returns {Vow} */ - getBrand() { - // XXX this is a common use case that should have a helper like `provideSingleton` - if (this.state.brandCache) return watch(this.state.brandCache); - return watch( - E(timerService).getTimerBrand(), - Far('BrandWatcher', { - /** @param {TimerBrand} timerBrand */ - onFulfilled: timerBrand => { - this.state.brandCache = timerBrand; - return timerBrand; - }, - }), - ); + getBrandWatcher: { + /** @param {TimerBrand} timerBrand */ + onFulfilled(timerBrand) { + this.state.brandCache = timerBrand; + return timerBrand; + }, }, - /** - * Takes the current time from ChainTimerService and adds a relative time - * to determine a timeout timestamp in nanoseconds. Useful for - * {@link MsgTransfer.timeoutTimestamp}. - * - * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes - * @returns {Vow} Timeout timestamp in absolute nanoseconds since - * unix epoch - */ - getTimeoutTimestampNS(relativeTime) { - return watch( - allVows([ - E(timerService).getCurrentTimestamp(), - this.self.getBrand(), - ]), - Far('TimestampWatcher', { - /** @param {[TimestampRecord, TimerBrand]} results */ - onFulfilled([currentTime, timerBrand]) { - const timeout = - relativeTime || - TimeMath.coerceRelativeTimeRecord( - SECONDS_PER_MINUTE * 5n, - timerBrand, - ); - return ( - TimeMath.addAbsRel(currentTime, timeout).absValue * - NANOSECONDS_PER_SECOND - ); - }, - }), - ); + getTimestampWatcher: { + /** + * @param {[TimestampRecord, TimerBrand]} results + * @param {{ relativeTime: RelativeTimeRecord }} ctx + */ + onFulfilled([currentTime, timerBrand], { relativeTime }) { + const timeout = + relativeTime || + TimeMath.coerceRelativeTimeRecord( + SECONDS_PER_MINUTE * 5n, + timerBrand, + ); + return ( + TimeMath.addAbsRel(currentTime, timeout).absValue * + NANOSECONDS_PER_SECOND + ); + }, + }, + public: { + /** @returns {Vow} */ + getBrand() { + // XXX this is a common use case that should have a helper like `provideSingleton` + if (this.state.brandCache) return watch(this.state.brandCache); + return watch( + E(timerService).getTimerBrand(), + this.facets.getBrandWatcher, + ); + }, + /** + * Takes the current time from ChainTimerService and adds a relative + * time to determine a timeout timestamp in nanoseconds. Useful for + * {@link MsgTransfer.timeoutTimestamp}. + * + * @param {RelativeTimeRecord} [relativeTime] defaults to 5 minutes + * @returns {Vow} Timeout timestamp in absolute nanoseconds + * since unix epoch + */ + getTimeoutTimestampNS(relativeTime) { + return watch( + allVows([ + E(timerService).getCurrentTimestamp(), + this.facets.public.getBrand(), + ]), + this.facets.getTimestampWatcher, + { relativeTime }, + ); + }, }, }, ); - return timeHelper(); +harden(prepareTimeHelperKit); + +/** + * @param {Zone} zone + * @param {TimeHelperPowers} powers + */ +export const prepareTimeHelper = (zone, powers) => { + const makeTimeHelperKit = prepareTimeHelperKit(zone, powers); + const makeTimeHelper = pickFacet(makeTimeHelperKit, 'public'); + const timeHelper = makeTimeHelper(); + return harden(timeHelper); }; -harden(makeTimeHelper); +harden(prepareTimeHelper); -/** @typedef {ReturnType} TimeHelper */ +/** @typedef {ReturnType} TimeHelper */ diff --git a/packages/orchestration/src/utils/start-helper.js b/packages/orchestration/src/utils/start-helper.js index 1254c6ce731..041b9ac5ec6 100644 --- a/packages/orchestration/src/utils/start-helper.js +++ b/packages/orchestration/src/utils/start-helper.js @@ -9,7 +9,7 @@ import { makeChainHub } from '../exos/chain-hub.js'; import { prepareRemoteChainFacade } from '../exos/remote-chain-facade.js'; import { prepareCosmosOrchestrationAccount } from '../exos/cosmos-orchestration-account.js'; import { prepareLocalChainFacade } from '../exos/local-chain-facade.js'; -import { makeTimeHelper } from '../exos/time-helper.js'; +import { prepareTimeHelper } from '../exos/time-helper.js'; /** * @import {PromiseKit} from '@endo/promise-kit' @@ -50,7 +50,7 @@ export const provideOrchestration = ( const vowTools = prepareVowTools(zone.subZone('vows')); const { agoricNames, timerService } = remotePowers; const chainHub = makeChainHub(agoricNames); - const timeHelper = makeTimeHelper(makeHeapZone(), { + const timeHelper = prepareTimeHelper(makeHeapZone(), { timerService, vowTools, }); diff --git a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts index 8660dc5aa97..f3212aac26d 100644 --- a/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-orchestration-account-kit.test.ts @@ -10,7 +10,7 @@ import { prepareLocalOrchestrationAccountKit } from '../../src/exos/local-orches import { ChainAddress } from '../../src/orchestration-api.js'; import { makeChainHub } from '../../src/exos/chain-hub.js'; import { - makeTimeHelper, + prepareTimeHelper, NANOSECONDS_PER_SECOND, } from '../../src/exos/time-helper.js'; import { commonSetup } from '../supports.js'; @@ -38,7 +38,7 @@ test('deposit, withdraw', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), - makeTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), + prepareTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), ); t.log('request account from vat-localchain'); @@ -110,7 +110,7 @@ test('delegate, undelegate', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), - makeTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), + prepareTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), ); t.log('request account from vat-localchain'); @@ -163,7 +163,7 @@ test('transfer', async t => { timer, vowTools, makeChainHub(bootstrap.agoricNames), - makeTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), + prepareTimeHelper(makeHeapZone(), { timerService: timer, vowTools }), ); t.log('request account from vat-localchain'); diff --git a/packages/orchestration/test/exos/time-helper.test.ts b/packages/orchestration/test/exos/time-helper.test.ts index 691cb054de9..515dc2879e4 100644 --- a/packages/orchestration/test/exos/time-helper.test.ts +++ b/packages/orchestration/test/exos/time-helper.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @jessie.js/safe-await-separator */ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; -import { prepareVowTools, V } from '@agoric/vow/vat.js'; +import { prepareVowTools, heapVowE } from '@agoric/vow/vat.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { TimeMath } from '@agoric/time'; import { makeHeapZone } from '@agoric/zone'; import { - makeTimeHelper, + prepareTimeHelper, NANOSECONDS_PER_SECOND, SECONDS_PER_MINUTE, } from '../../src/exos/time-helper.js'; @@ -16,23 +16,24 @@ test('makeTimeHelper - getCurrentTimestamp', async t => { const timerBrand = timer.getTimerBrand(); t.is(timer.getCurrentTimestamp().absValue, 0n, 'current time is 0n'); - const timeHelper = makeTimeHelper(makeHeapZone(), { + const timeHelper = prepareTimeHelper(makeHeapZone(), { timerService: timer, vowTools, }); + t.is( - await V.when(timeHelper.getTimeoutTimestampNS()), + await heapVowE.when(timeHelper.getTimeoutTimestampNS()), 5n * SECONDS_PER_MINUTE * NANOSECONDS_PER_SECOND, 'default timestamp is 5 minutes from current time, in nanoseconds', ); t.is( - await V.when(timeHelper.getBrand()), + await heapVowE.when(timeHelper.getBrand()), timerBrand, 'brand retrieved cache equals timerBrand', ); t.is( - await V.when( + await heapVowE.when( timeHelper.getTimeoutTimestampNS( TimeMath.coerceRelativeTimeRecord(1n, timerBrand), ), @@ -44,7 +45,7 @@ test('makeTimeHelper - getCurrentTimestamp', async t => { // advance timer by 3 seconds await timer.tickN(3); t.is( - await V.when( + await heapVowE.when( timeHelper.getTimeoutTimestampNS( TimeMath.coerceRelativeTimeRecord(1n, timerBrand), ),