diff --git a/a3p-integration/proposals/z:acceptance/.gitignore b/a3p-integration/proposals/z:acceptance/.gitignore index 3d143254692..7f5da265d56 100644 --- a/a3p-integration/proposals/z:acceptance/.gitignore +++ b/a3p-integration/proposals/z:acceptance/.gitignore @@ -2,3 +2,4 @@ restart-valueVow start-valueVow localchaintest-submission +recorded-instances-submission diff --git a/a3p-integration/proposals/z:acceptance/package.json b/a3p-integration/proposals/z:acceptance/package.json index 9b7968892e3..3b19c101aa7 100644 --- a/a3p-integration/proposals/z:acceptance/package.json +++ b/a3p-integration/proposals/z:acceptance/package.json @@ -3,6 +3,7 @@ "type": "/agoric.swingset.CoreEvalProposal", "sdk-generate": [ "testing/start-valueVow.js start-valueVow", + "testing/recorded-retired-instances.js recorded-instances-submission", "vats/test-localchain.js localchaintest-submission", "testing/restart-valueVow.js restart-valueVow" ] diff --git a/a3p-integration/proposals/z:acceptance/recorded-retired.test.js b/a3p-integration/proposals/z:acceptance/recorded-retired.test.js new file mode 100644 index 00000000000..a5a00d01107 --- /dev/null +++ b/a3p-integration/proposals/z:acceptance/recorded-retired.test.js @@ -0,0 +1,11 @@ +import test from 'ava'; + +import { evalBundles } from '@agoric/synthetic-chain'; + +const SUBMISSION_DIR = 'recorded-instances-submission'; + +test(`recorded instances in u18`, async t => { + const result = await evalBundles(SUBMISSION_DIR); + console.log('recorded retired instance result:', result); + t.pass('checked names'); +}); diff --git a/a3p-integration/proposals/z:acceptance/test.sh b/a3p-integration/proposals/z:acceptance/test.sh index ee4e473496d..dce7152b5b7 100755 --- a/a3p-integration/proposals/z:acceptance/test.sh +++ b/a3p-integration/proposals/z:acceptance/test.sh @@ -13,6 +13,9 @@ yarn ava core-eval.test.js npm install -g tsx scripts/test-vaults.mts +echo ACCEPTANCE TESTING recorded instances +yarn ava recorded-retired.test.js + echo ACCEPTANCE TESTING kread ./create-kread-item-test.sh diff --git a/packages/boot/test/bootstrapTests/price-feed-replace.test.ts b/packages/boot/test/bootstrapTests/price-feed-replace.test.ts index a9c0ce6e177..0888a9bbcfd 100644 --- a/packages/boot/test/bootstrapTests/price-feed-replace.test.ts +++ b/packages/boot/test/bootstrapTests/price-feed-replace.test.ts @@ -101,7 +101,7 @@ test.serial('setupVaults; run updatePriceFeeds proposals', async t => { t.log('building all relevant CoreEvals'); const coreEvals = await Promise.all([ - buildProposal(priceFeedBuilder, ['MAINNET']), + buildProposal(priceFeedBuilder, ['BOOT_TEST']), buildProposal('@agoric/builders/scripts/vats/upgradeVaults.js'), buildProposal('@agoric/builders/scripts/vats/add-auction.js'), ]); diff --git a/packages/boot/test/bootstrapTests/updateGovernedParams.test.ts b/packages/boot/test/bootstrapTests/updateGovernedParams.test.ts index 5410fcc59d7..07c4e711289 100644 --- a/packages/boot/test/bootstrapTests/updateGovernedParams.test.ts +++ b/packages/boot/test/bootstrapTests/updateGovernedParams.test.ts @@ -134,7 +134,7 @@ test('modify manager & director params; update vats, check', async t => { const priceFeedBuilder = '@agoric/builders/scripts/inter-protocol/updatePriceFeeds.js'; const coreEvals = await Promise.all([ - buildProposal(priceFeedBuilder, ['MAINNET']), + buildProposal(priceFeedBuilder, ['BOOT_TEST']), buildProposal('@agoric/builders/scripts/vats/upgradeVaults.js'), buildProposal('@agoric/builders/scripts/vats/add-auction.js'), ]); diff --git a/packages/builders/scripts/inter-protocol/updatePriceFeeds.js b/packages/builders/scripts/inter-protocol/updatePriceFeeds.js index f5e71a6c137..f99db1caa59 100644 --- a/packages/builders/scripts/inter-protocol/updatePriceFeeds.js +++ b/packages/builders/scripts/inter-protocol/updatePriceFeeds.js @@ -41,6 +41,16 @@ const configurations = { ], inBrandNames: ['ATOM', 'stATOM', 'stOSMO', 'stTIA', 'stkATOM'], }, + BOOT_TEST: { + oracleAddresses: [ + 'agoric144rrhh4m09mh7aaffhm6xy223ym76gve2x7y78', // DSRV + 'agoric19d6gnr9fyp6hev4tlrg87zjrzsd5gzr5qlfq2p', // Stakin + 'agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8', // 01node + 'agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr', // Simply Staking + 'agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj', // P2P + ], + inBrandNames: ['ATOM'], + }, }; const { keys } = Object; diff --git a/packages/builders/scripts/testing/recorded-retired-instances.js b/packages/builders/scripts/testing/recorded-retired-instances.js new file mode 100644 index 00000000000..58bc79c969e --- /dev/null +++ b/packages/builders/scripts/testing/recorded-retired-instances.js @@ -0,0 +1,73 @@ +import { makeTracer } from '@agoric/internal'; +import { E } from '@endo/far'; + +const trace = makeTracer('RecordedRetired', true); + +/** + * @param {BootstrapPowers & + * PromiseSpaceOf<{ retiredContractInstances: MapStore; + * }> + * } powers + */ +export const testRecordedRetiredInstances = async ({ + consume: { + contractKits, + // governedContractKits, + retiredContractInstances: retiredContractInstancesP, + }, +}) => { + trace('Start'); + const retiredContractInstances = await retiredContractInstancesP; + + const auctionIDs = Array.from(retiredContractInstances.keys()).filter(k => + k.startsWith('auction'), + ); + assert(auctionIDs); + assert(auctionIDs.length === 1); + const auctionInstance = retiredContractInstances.get(auctionIDs[0]); + trace({ auctionInstance }); + // I don't know why it's neither in governedContractKits nor contractKits + // assert(await E(governedContractKits).get(auctionInstance)); + + const committeeIDs = Array.from(retiredContractInstances.keys()).filter(k => + k.startsWith('economicCommittee'), + ); + assert(committeeIDs); + assert(committeeIDs.length === 1); + const committeeInstance = retiredContractInstances.get(committeeIDs[0]); + assert(await E(contractKits).get(committeeInstance)); + + trace('done'); +}; +harden(testRecordedRetiredInstances); + +export const getManifestForRecordedRetiredInstances = () => { + return { + manifest: { + [testRecordedRetiredInstances.name]: { + consume: { + contractKits: true, + // governedContractKits: true, + retiredContractInstances: true, + }, + }, + }, + }; +}; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ +export const defaultProposalBuilder = async () => + harden({ + sourceSpec: + '@agoric/builders/scripts/testing/recorded-retired-instances.js', + getManifestCall: ['getManifestForRecordedRetiredInstances', {}], + }); + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').DeployScriptFunction} */ +export default async (homeP, endowments) => { + // import dynamically so the module can work in CoreEval environment + const dspModule = await import('@agoric/deploy-script-support'); + const { makeHelpers } = dspModule; + const { writeCoreEval } = await makeHelpers(homeP, endowments); + await writeCoreEval('recorded-retired', defaultProposalBuilder); +}; diff --git a/packages/inter-protocol/src/proposals/add-auction.js b/packages/inter-protocol/src/proposals/add-auction.js index 6814fbdcc87..76dd57a0c6b 100644 --- a/packages/inter-protocol/src/proposals/add-auction.js +++ b/packages/inter-protocol/src/proposals/add-auction.js @@ -1,8 +1,9 @@ import { deeplyFulfilledObject, makeTracer } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; -import { E } from '@endo/far'; import { Stable } from '@agoric/internal/src/tokens.js'; +import { E } from '@endo/far'; import { makeGovernedTerms as makeGovernedATerms } from '../auction/params.js'; +import { provideRetiredInstances } from './utils.js'; const trace = makeTracer('NewAuction', true); @@ -11,6 +12,7 @@ const trace = makeTracer('NewAuction', true); * auctionUpgradeNewInstance: Instance; * auctionUpgradeNewGovCreator: any; * newContractGovBundleId: string; + * retiredContractInstances: MapStore; * }>} interlockPowers */ @@ -35,6 +37,7 @@ export const addAuction = async ( economicCommitteeCreatorFacet: electorateCreatorFacet, governedContractKits: governedContractKitsP, priceAuthority8400, + retiredContractInstances: retiredContractInstancesP, zoe, }, produce: { @@ -42,6 +45,7 @@ export const addAuction = async ( auctionUpgradeNewInstance, auctionUpgradeNewGovCreator, newContractGovBundleId, + retiredContractInstances: produceRetiredInstances, }, instance: { consume: { reserve: reserveInstance }, @@ -79,6 +83,16 @@ export const addAuction = async ( auctioneerInstallationP, ]); + const retiredInstances = await provideRetiredInstances( + retiredContractInstancesP, + produceRetiredInstances, + ); + + // save the auctioneer instance so we can manage it later + const boardID = await E(board).getId(legacyKit.instance); + const identifier = `auctioneer-${boardID}`; + retiredInstances.init(identifier, legacyKit.instance); + // Each field has an extra layer of type + value: // AuctionStartDelay: { type: 'relativeTime', value: { relValue: 2n, timerBrand: Object [Alleged: timerBrand] {} } } /** @type {any} */ @@ -210,6 +224,7 @@ export const ADD_AUCTION_MANIFEST = harden({ economicCommitteeCreatorFacet: true, governedContractKits: true, priceAuthority8400: true, + retiredContractInstances: true, zoe: true, }, produce: { @@ -217,6 +232,7 @@ export const ADD_AUCTION_MANIFEST = harden({ auctionUpgradeNewInstance: true, auctionUpgradeNewGovCreator: true, newContractGovBundleId: true, + retiredContractInstances: true, }, instance: { consume: { reserve: true }, diff --git a/packages/inter-protocol/src/proposals/deploy-price-feeds.js b/packages/inter-protocol/src/proposals/deploy-price-feeds.js index f98f62223f2..0ecbea49098 100644 --- a/packages/inter-protocol/src/proposals/deploy-price-feeds.js +++ b/packages/inter-protocol/src/proposals/deploy-price-feeds.js @@ -5,6 +5,7 @@ import { E } from '@endo/far'; import { unitAmount } from '@agoric/zoe/src/contractSupport/priceQuote.js'; import { oracleBrandFeedName, + provideRetiredInstances, reserveThenDeposit, sanitizePathSegment, } from './utils.js'; @@ -84,7 +85,8 @@ export const ensureOracleBrand = async ( }; /** - * @param {EconomyBootstrapPowers} powers + * @param {EconomyBootstrapPowers & + * PromiseSpaceOf<{ retiredContractInstances: MapStore }>} powers * @param {{ * AGORIC_INSTANCE_NAME: string; * contractTerms: import('@agoric/inter-protocol/src/price/fluxAggregatorKit.js').ChainlinkConfig; @@ -96,6 +98,7 @@ export const ensureOracleBrand = async ( const startPriceAggregatorInstance = async ( { consume: { + agoricNames, board, chainStorage, chainTimerService, @@ -103,8 +106,10 @@ const startPriceAggregatorInstance = async ( highPrioritySendersManager, namesByAddressAdmin, startGovernedUpgradable, + retiredContractInstances: retiredContractInstancesP, }, instance: { produce: produceInstance }, + produce: { retiredContractInstances: produceRetiredInstances }, }, { AGORIC_INSTANCE_NAME, contractTerms, brandIn, brandOut }, installation, @@ -139,6 +144,22 @@ const startPriceAggregatorInstance = async ( // @ts-expect-error GovernableStartFn vs. fluxAggregatorContract.js start installation, }); + const retiredContractInstances = await provideRetiredInstances( + retiredContractInstancesP, + produceRetiredInstances, + ); + + // save the instance so we can manage it later + const retiringInstance = await E(agoricNames).lookup( + 'instance', + AGORIC_INSTANCE_NAME, + ); + const boardID = await E(board).getId(retiringInstance); + retiredContractInstances.init( + `priceFeed-${AGORIC_INSTANCE_NAME}-${boardID}`, + retiringInstance, + ); + produceInstance[AGORIC_INSTANCE_NAME].reset(); produceInstance[AGORIC_INSTANCE_NAME].resolve(governedKit.instance); trace( @@ -191,7 +212,9 @@ const distributeInvitations = async ( }; /** - * @param {EconomyBootstrapPowers & NamedVatPowers} powers + * @param {EconomyBootstrapPowers & + * NamedVatPowers & + * PromiseSpaceOf<{ retiredContractInstances: MapStore }>} powers * @param {{ * options: PriceFeedConfig & { * priceAggregatorRef: { bundleID: string }; @@ -300,6 +323,7 @@ export const getManifestForPriceFeeds = async ( namesByAddressAdmin: t, priceAuthority: t, priceAuthorityAdmin: t, + retiredContractInstances: t, startGovernedUpgradable: t, startUpgradable: t, zoe: t, @@ -307,9 +331,10 @@ export const getManifestForPriceFeeds = async ( installation: { produce: { priceAggregator: t } }, instance: { produce: t, + consume: t, }, oracleBrand: { produce: t }, - produce: { priceAuthority8400: t }, + produce: { priceAuthority8400: t, retiredContractInstances: t }, }, }, options: { ...priceFeedOptions }, diff --git a/packages/inter-protocol/src/proposals/replaceElectorate.js b/packages/inter-protocol/src/proposals/replaceElectorate.js index cb862be5a67..adde9adf747 100644 --- a/packages/inter-protocol/src/proposals/replaceElectorate.js +++ b/packages/inter-protocol/src/proposals/replaceElectorate.js @@ -15,7 +15,7 @@ import { assertPathSegment, makeStorageNodeChild, } from '@agoric/internal/src/lib-chainStorage.js'; -import { reserveThenDeposit } from './utils.js'; +import { provideRetiredInstances, reserveThenDeposit } from './utils.js'; /** @import {EconomyBootstrapPowers} from './econ-behaviors.js' */ /** @import {EconCharterStartResult} from './econ-behaviors.js' */ @@ -181,8 +181,10 @@ const inviteToEconCharter = async ( * Starts a new Economic Committee (EC) by creating an instance with the * provided committee specifications. * - * @param {EconomyBootstrapPowers} powers - The resources and capabilities - * required to start the committee. + * @param {EconomyBootstrapPowers & + * PromiseSpaceOf<{ retiredContractInstances: MapStore }>} powers + * - The resources and capabilities required to start the committee. + * * @param {{ * options: { * committeeName: string; @@ -196,12 +198,22 @@ const inviteToEconCharter = async ( */ const startNewEconomicCommittee = async ( { - consume: { board, chainStorage, startUpgradable }, - produce: { economicCommitteeKit, economicCommitteeCreatorFacet }, + consume: { + board, + chainStorage, + startUpgradable, + retiredContractInstances: retiredInstancesP, + }, + produce: { + economicCommitteeKit, + economicCommitteeCreatorFacet, + retiredContractInstances: produceRetiredInstances, + }, installation: { consume: { committee }, }, instance: { + consume: { economicCommittee: economicCommitteeOriginalP }, produce: { economicCommittee }, }, }, @@ -214,6 +226,19 @@ const startNewEconomicCommittee = async ( trace(`committeeName ${committeeName}`); trace(`committeeSize ${committeeSize}`); + const retiredInstances = await provideRetiredInstances( + retiredInstancesP, + produceRetiredInstances, + ); + + // Record the retired electorate instance so we can manage it later. + const economicCommitteeOriginal = await economicCommitteeOriginalP; + const boardID = await E(board).getId(economicCommitteeOriginal); + retiredInstances.init( + `economicCommittee-${boardID}`, + economicCommitteeOriginal, + ); + const committeesNode = await makeStorageNodeChild( chainStorage, COMMITTEES_ROOT, @@ -309,6 +334,7 @@ const startNewEconCharter = async ({ * @typedef {PromiseSpaceOf<{ * auctionUpgradeNewInstance: Instance; * auctionUpgradeNewGovCreator: any; + * retiredContractInstances: MapStore; * }>} interlockPowers */ @@ -485,6 +511,7 @@ export const getManifestForReplaceAllElectorates = async ( manifest: { [replaceAllElectorates.name]: { consume: { + agoricNames: true, auctionUpgradeNewGovCreator: true, auctionUpgradeNewInstance: true, psmKit: true, @@ -492,6 +519,7 @@ export const getManifestForReplaceAllElectorates = async ( chainStorage: true, highPrioritySendersManager: true, namesByAddressAdmin: true, + retiredContractInstances: true, // Rest of these are designed to be widely shared board: true, startUpgradable: true, @@ -501,6 +529,7 @@ export const getManifestForReplaceAllElectorates = async ( economicCommitteeKit: true, economicCommitteeCreatorFacet: true, auctionUpgradeNewGovCreator: true, + retiredContractInstances: true, }, installation: { consume: { @@ -514,6 +543,7 @@ export const getManifestForReplaceAllElectorates = async ( economicCommittee: true, econCommitteeCharter: true, }, + consume: { economicCommittee: true }, }, }, }, diff --git a/packages/inter-protocol/src/proposals/utils.js b/packages/inter-protocol/src/proposals/utils.js index 42894a27148..165731e4838 100644 --- a/packages/inter-protocol/src/proposals/utils.js +++ b/packages/inter-protocol/src/proposals/utils.js @@ -3,6 +3,7 @@ import { E } from '@endo/far'; import { WalletName } from '@agoric/internal'; import { getCopyMapEntries, makeCopyMap } from '@agoric/store'; import { assertPathSegment } from '@agoric/internal/src/lib-chainStorage.js'; +import { makeScalarBigMapStore } from '@agoric/vat-data'; /** @import {CopyMap} from '@endo/patterns'; */ @@ -171,3 +172,22 @@ export const sanitizePathSegment = name => { assertPathSegment(candidate); return candidate; }; + +/** + * Idempotently provide an empty MapStore for the `retiredContractInstances` + * value in promise space + * + * @param {Promise} consume + * @param {Producer} produce + * @returns {Promise} + */ +export const provideRetiredInstances = async (consume, produce) => { + // Promise space has no way to look for an existing value other than awaiting a promise, + // but it does allow extra production so it's safe to do this redundantly. + produce.resolve( + makeScalarBigMapStore('retiredContractInstances', { + durable: true, + }), + ); + return consume; +}; diff --git a/packages/inter-protocol/src/vaultFactory/vaultManager.js b/packages/inter-protocol/src/vaultFactory/vaultManager.js index 474d7843927..8979943693f 100644 --- a/packages/inter-protocol/src/vaultFactory/vaultManager.js +++ b/packages/inter-protocol/src/vaultFactory/vaultManager.js @@ -227,6 +227,7 @@ export const prepareVaultManagerKit = ( { zcf, marshaller, makeRecorderKit, factoryPowers }, ) => { const { priceAuthority, timerService, reservePublicFacet } = zcf.getTerms(); + const watchedBrands = new Set(); const makeVault = prepareVault(baggage, makeRecorderKit, zcf); @@ -424,52 +425,69 @@ export const prepareVaultManagerKit = ( }, }); - void facets.helper.observeQuoteNotifier(); + void facets.helper.ensureQuoteNotifierWatched(); trace('helper.start() done'); }, - observeQuoteNotifier() { + ensureQuoteNotifierWatched() { const { state } = this; const { collateralBrand, collateralUnit, debtBrand, storageNode } = state; + if (watchedBrands.has(collateralBrand)) { + return; + } + watchedBrands.add(collateralBrand); + const ephemera = collateralEphemera(collateralBrand); - const quoteNotifier = E(priceAuthority).makeQuoteNotifier( + const quoteNotifierP = E(priceAuthority).makeQuoteNotifier( collateralUnit, debtBrand, ); - // @ts-expect-error XXX quotes - ephemera.storedQuotesNotifier = makeStoredNotifier( - // @ts-expect-error XXX quotes - quoteNotifier, - E(storageNode).makeChildNode('quotes'), - marshaller, - ); - trace( - 'helper.start() awaiting observe storedQuotesNotifier', - collateralBrand, - ); - // NB: upon restart, there may not be a price for a while. If manager - // operations are permitted, ones that depend on price information - // will throw. See https://github.com/Agoric/agoric-sdk/issues/4317 - const quoteWatcher = harden({ - onFulfilled(value) { - trace('watcher updated price', value); - ephemera.storedCollateralQuote = value; - }, - onRejected() { - // NOTE: drastic action, if the quoteNotifier fails, we don't know - // the value of the asset, nor do we know how long we'll be in - // ignorance. Best choice is to disable actions that require - // prices and restart when we have a new price. If we restart the - // notifier immediately, we'll trigger an infinite loop, so try - // to restart each time we get a request. + void E.when( + quoteNotifierP, + quoteNotifier => { + // @ts-expect-error XXX quotes + ephemera.storedQuotesNotifier = makeStoredNotifier( + // @ts-expect-error XXX quotes + quoteNotifier, + E(storageNode).makeChildNode('quotes'), + marshaller, + ); + trace( + 'helper.start() awaiting observe storedQuotesNotifier', + collateralBrand, + ); + // NB: upon restart, there may not be a price for a while. If manager + // operations are permitted, ones that depend on price information + // will throw. See https://github.com/Agoric/agoric-sdk/issues/4317 + const quoteWatcher = harden({ + onFulfilled(value) { + trace('watcher updated price', value); + ephemera.storedCollateralQuote = value; + }, + onRejected() { + // NOTE: drastic action, if the quoteNotifier fails, we don't know + // the value of the asset, nor do we know how long we'll be in + // ignorance. Best choice is to disable actions that require + // prices and restart when we have a new price. If we restart the + // notifier immediately, we'll trigger an infinite loop, so try + // to restart each time we get a request. + + ephemera.storedCollateralQuote = null; + watchedBrands.delete(collateralBrand); + }, + }); + void watchQuoteNotifier(quoteNotifier, quoteWatcher); + }, + e => { + trace('makeQuoteNotifier failed, resetting', e); ephemera.storedCollateralQuote = null; + watchedBrands.delete(collateralBrand); }, - }); - void watchQuoteNotifier(quoteNotifier, quoteWatcher); + ); }, /** @param {Timestamp} updateTime */ async chargeAllVaults(updateTime) { @@ -841,7 +859,7 @@ export const prepareVaultManagerKit = ( const { collateralBrand } = state; const { storedCollateralQuote } = collateralEphemera(collateralBrand); if (!storedCollateralQuote) { - facets.helper.observeQuoteNotifier(); + facets.helper.ensureQuoteNotifierWatched(); // it might take an arbitrary amount of time to get a new quote throw Fail`maxDebtFor called before a collateral quote was available for ${collateralBrand}`; @@ -1088,7 +1106,7 @@ export const prepareVaultManagerKit = ( state.collateralBrand, ); if (!storedCollateralQuote) { - facets.helper.observeQuoteNotifier(); + facets.helper.ensureQuoteNotifierWatched(); // it might take an arbitrary amount of time to get a new quote throw Fail`getCollateralQuote called before a collateral quote was available`; @@ -1107,7 +1125,7 @@ export const prepareVaultManagerKit = ( state.collateralBrand, ); if (!storedCollateralQuote) { - facets.helper.observeQuoteNotifier(); + facets.helper.ensureQuoteNotifierWatched(); // it might take an arbitrary amount of time to get a new quote throw Fail`lockOraclePrices called before a collateral quote was available for ${state.collateralBrand}`; diff --git a/packages/telemetry/src/context-aware-slog.js b/packages/telemetry/src/context-aware-slog.js index dd55f418734..ee17acf3dc0 100644 --- a/packages/telemetry/src/context-aware-slog.js +++ b/packages/telemetry/src/context-aware-slog.js @@ -219,7 +219,7 @@ export const makeContextualSlogProcessor = ( triggerContext = { 'run.num': undefined, - 'run.id': `${triggerType}-${finalBody.inboundNum}`, + 'run.id': `${triggerType}-${finalBody.blockHeight}`, 'run.trigger.type': triggerType, 'run.trigger.time': finalBody.blockTime, 'run.trigger.blockHeight': finalBody.blockHeight,