diff --git a/a3p-integration/proposals/a:upgrade-16/package.json b/a3p-integration/proposals/a:upgrade-16/package.json index c8b0d937fe31..9ee2426a558e 100644 --- a/a3p-integration/proposals/a:upgrade-16/package.json +++ b/a3p-integration/proposals/a:upgrade-16/package.json @@ -2,7 +2,7 @@ "agoricProposal": { "releaseNotes": false, "sdkImageTag": "unreleased", - "planName": "agoric-upgrade-16", + "planName": "agoric-upgrade-16-a3p-integration", "upgradeInfo": { "coreProposals": [] }, diff --git a/golang/cosmos/app/app.go b/golang/cosmos/app/app.go index 4f87a0c67c41..78f6b35cc295 100644 --- a/golang/cosmos/app/app.go +++ b/golang/cosmos/app/app.go @@ -110,6 +110,8 @@ import ( appante "github.com/Agoric/agoric-sdk/golang/cosmos/ante" agorictypes "github.com/Agoric/agoric-sdk/golang/cosmos/types" + + // conv "github.com/Agoric/agoric-sdk/golang/cosmos/types/conv" "github.com/Agoric/agoric-sdk/golang/cosmos/vm" "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset" swingsetclient "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/client" @@ -907,86 +909,6 @@ func NewAgoricApp( return app } -var upgradeNamesOfThisVersion = map[string]bool{ - "agoric-upgrade-16": true, - "agorictest-upgrade-16": true, -} - -func isFirstTimeUpgradeOfThisVersion(app *GaiaApp, ctx sdk.Context) bool { - for name := range upgradeNamesOfThisVersion { - if app.UpgradeKeeper.GetDoneHeight(ctx, name) != 0 { - return false - } - } - return true -} - -// upgrade16Handler performs standard upgrade actions plus custom actions for upgrade-16. -func upgrade16Handler(app *GaiaApp, targetUpgrade string) func(sdk.Context, upgradetypes.Plan, module.VersionMap) (module.VersionMap, error) { - return func(ctx sdk.Context, plan upgradetypes.Plan, fromVm module.VersionMap) (module.VersionMap, error) { - app.CheckControllerInited(false) - - CoreProposalSteps := []vm.CoreProposalStep{} - - // These CoreProposalSteps are not idempotent and should only be executed - // as part of the first upgrade using this handler on any given chain. - if isFirstTimeUpgradeOfThisVersion(app, ctx) { - // Each CoreProposalStep runs sequentially, and can be constructed from - // one or more modules executing in parallel within the step. - CoreProposalSteps = []vm.CoreProposalStep{ - // Upgrade Zoe + ZCF - vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/replace-zoe.js"), - // Revive KREAd characters - vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/revive-kread.js"), - - // upgrade the provisioning vat - vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/replace-provisioning.js"), - // Enable low-level Orchestration. - vm.CoreProposalStepForModules( - "@agoric/builders/scripts/vats/init-network.js", - "@agoric/builders/scripts/vats/init-localchain.js", - "@agoric/builders/scripts/vats/init-transfer.js", - ), - // Add new vats for price feeds. The existing ones will be retired shortly. - vm.CoreProposalStepForModules( - "@agoric/builders/scripts/vats/updateAtomPriceFeed.js", - "@agoric/builders/scripts/vats/updateStAtomPriceFeed.js", - "@agoric/builders/scripts/vats/updateStOsmoPriceFeed.js", - "@agoric/builders/scripts/vats/updateStTiaPriceFeed.js", - "@agoric/builders/scripts/vats/updateStkAtomPriceFeed.js", - ), - // Add new auction contract. The old one will be retired shortly. - vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/add-auction.js"), - // upgrade vaultFactory. - vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/upgradeVaults.js"), - } - } - - app.upgradeDetails = &upgradeDetails{ - // Record the plan to send to SwingSet - Plan: plan, - // Core proposals that should run during the upgrade block - // These will be merged with any coreProposals specified in the - // upgradeInfo field of the upgrade plan ran as subsequent steps - CoreProposals: vm.CoreProposalsFromSteps(CoreProposalSteps...), - } - - // Always run module migrations - mvm, err := app.mm.RunMigrations(ctx, app.configurator, fromVm) - if err != nil { - return mvm, err - } - - m := swingsetkeeper.NewMigrator(app.SwingSetKeeper) - err = m.MigrateParams(ctx) - if err != nil { - return mvm, err - } - - return mvm, nil - } -} - // normalizeModuleAccount ensures that the given account is a module account, // initializing or updating it if necessary. The account name must be listed in maccPerms. func normalizeModuleAccount(ctx sdk.Context, ak authkeeper.AccountKeeper, name string) { diff --git a/golang/cosmos/app/upgrade.go b/golang/cosmos/app/upgrade.go new file mode 100644 index 000000000000..a0e73d6c3355 --- /dev/null +++ b/golang/cosmos/app/upgrade.go @@ -0,0 +1,212 @@ +package gaia + +import ( + "encoding/json" + "fmt" + "strings" + "text/template" + + "github.com/Agoric/agoric-sdk/golang/cosmos/vm" + swingsetkeeper "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/keeper" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" +) + +var upgradeNamesOfThisVersion = map[string]bool{ + "agoric-upgrade-16-basic": true, // no-frills + "agoric-upgrade-16-a3p-integration": true, + "agoric-upgrade-16-main": true, + "agoric-upgrade-16-devnet": true, +} + +func isFirstTimeUpgradeOfThisVersion(app *GaiaApp, ctx sdk.Context) bool { + for name := range upgradeNamesOfThisVersion { + if app.UpgradeKeeper.GetDoneHeight(ctx, name) != 0 { + return false + } + } + return true +} + +// upgradePriceFeedCoreProposalSteps returns the core proposal steps for the +// price feed upgrade and associated changes to scaledPriceAuthority and +// vaultManager. +func upgradePriceFeedCoreProposalSteps(upgradeName string) ([]vm.CoreProposalStep, error) { + isThisUpgrade := func(expectedUpgradeName string) bool { + return upgradeName == expectedUpgradeName + } + + t := template.Must(template.New("").Parse(`{ + "module": "@agoric/builders/scripts/vats/priceFeedSupport.js", + "entrypoint": {{.entrypointJson}}, + "args": [{ + "AGORIC_INSTANCE_NAME": {{.instanceNameJson}}, + "ORACLE_ADDRESSES": {{.oracleAddressesJson}}, + "IN_BRAND_LOOKUP": {{.inBrandLookupJson}}, + "IN_BRAND_DECIMALS": 6, + "OUT_BRAND_LOOKUP": ["agoricNames", "oracleBrand", "USD"], + "OUT_BRAND_DECIMALS": 4 + }] + }`)) + + var oracleAddresses []string + + var entrypoint string + switch { + case isThisUpgrade("agoric-upgrade-16-a3p-integration"): + entrypoint = "deprecatedPriceFeedProposalBuilder" + case isThisUpgrade("agoric-upgrade-16-main"): + oracleAddresses = []string{ + "agoric144rrhh4m09mh7aaffhm6xy223ym76gve2x7y78", // DSRV + "agoric19d6gnr9fyp6hev4tlrg87zjrzsd5gzr5qlfq2p", // Stakin + "agoric19uscwxdac6cf6z7d5e26e0jm0lgwstc47cpll8", // 01node + "agoric1krunjcqfrf7la48zrvdfeeqtls5r00ep68mzkr", // Simply Staking + "agoric1n4fcxsnkxe4gj6e24naec99hzmc4pjfdccy5nj", // P2P + } + entrypoint = "strictPriceFeedProposalBuilder" + case isThisUpgrade("agoric-upgrade-16-devnet"): + oracleAddresses = []string{ + "agoric1lw4e4aas9q84tq0q92j85rwjjjapf8dmnllnft", // DSRV + "agoric1zj6vrrrjq4gsyr9lw7dplv4vyejg3p8j2urm82", // Stakin + "agoric1ra0g6crtsy6r3qnpu7ruvm7qd4wjnznyzg5nu4", // 01node + "agoric1qj07c7vfk3knqdral0sej7fa6eavkdn8vd8etf", // Simply Staking + "agoric10vjkvkmpp9e356xeh6qqlhrny2htyzp8hf88fk", // P2P + } + entrypoint = "strictPriceFeedProposalBuilder" + + // No price feed upgrade for this version. + case isThisUpgrade("agoric-upgrade-16-basic"): + } + + if entrypoint == "" { + return []vm.CoreProposalStep{}, nil + } + + entrypointJson, err := json.Marshal(entrypoint) + if err != nil { + return nil, err + } + + var inBrandNames []string + switch { + case isThisUpgrade("agoric-upgrade-16-a3p-integration"), isThisUpgrade("agoric-upgrade-16-main"): + inBrandNames = []string{ + "ATOM", + "stATOM", + "stOSMO", + "stTIA", + "stkATOM", + } + case isThisUpgrade("agoric-upgrade-16-devnet"): + inBrandNames = []string{ + "ATOM", + "stTIA", + "stkATOM", + } + } + + oracleAddressesJson, err := json.Marshal(oracleAddresses) + if err != nil { + return nil, err + } + + proposals := make(vm.CoreProposalStep, 0, len(inBrandNames)) + for _, inBrandName := range inBrandNames { + instanceName := inBrandName + "-USD price feed" + instanceNameJson, err := json.Marshal(instanceName) + if err != nil { + return nil, err + } + inBrandLookup := []string{"agoricNames", "oracleBrand", inBrandName} + inBrandLookupJson, err := json.Marshal(inBrandLookup) + if err != nil { + return nil, err + } + + var result strings.Builder + err = t.Execute(&result, map[string]any{ + "entrypointJson": string(entrypointJson), + "inBrandLookupJson": string(inBrandLookupJson), + "instanceNameJson": string(instanceNameJson), + "oracleAddressesJson": string(oracleAddressesJson), + }) + if err != nil { + return nil, err + } + + jsonStr := result.String() + jsonBz := []byte(jsonStr) + if !json.Valid(jsonBz) { + return nil, fmt.Errorf("invalid JSON: %s", jsonStr) + } + proposals = append(proposals, vm.ArbitraryCoreProposal{Json: jsonBz}) + } + return []vm.CoreProposalStep{ + // Add new vats for price feeds. The existing ones will be retired shortly. + vm.CoreProposalStepForModules(proposals...), + // Add new auction contract. The old one will be retired shortly. + vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/add-auction.js"), + // upgrade vaultFactory. + vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/upgradeVaults.js"), + }, nil +} + +// upgrade16Handler performs standard upgrade actions plus custom actions for upgrade-16. +func upgrade16Handler(app *GaiaApp, targetUpgrade string) func(sdk.Context, upgradetypes.Plan, module.VersionMap) (module.VersionMap, error) { + return func(ctx sdk.Context, plan upgradetypes.Plan, fromVm module.VersionMap) (module.VersionMap, error) { + app.CheckControllerInited(false) + + CoreProposalSteps := []vm.CoreProposalStep{} + + // These CoreProposalSteps are not idempotent and should only be executed + // as part of the first upgrade using this handler on any given chain. + if isFirstTimeUpgradeOfThisVersion(app, ctx) { + // Each CoreProposalStep runs sequentially, and can be constructed from + // one or more modules executing in parallel within the step. + CoreProposalSteps = []vm.CoreProposalStep{ + // Upgrade Zoe + ZCF + vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/replace-zoe.js"), + // Revive KREAd characters + vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/revive-kread.js"), + + // upgrade the provisioning vat + vm.CoreProposalStepForModules("@agoric/builders/scripts/vats/replace-provisioning.js"), + // Enable low-level Orchestration. + vm.CoreProposalStepForModules( + "@agoric/builders/scripts/vats/init-network.js", + "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js", + ), + } + priceFeedSteps, err := upgradePriceFeedCoreProposalSteps(targetUpgrade) + if err != nil { + return nil, err + } + CoreProposalSteps = append(CoreProposalSteps, priceFeedSteps...) + } + + app.upgradeDetails = &upgradeDetails{ + // Record the plan to send to SwingSet + Plan: plan, + // Core proposals that should run during the upgrade block + // These will be merged with any coreProposals specified in the + // upgradeInfo field of the upgrade plan ran as subsequent steps + CoreProposals: vm.CoreProposalsFromSteps(CoreProposalSteps...), + } + + // Always run module migrations + mvm, err := app.mm.RunMigrations(ctx, app.configurator, fromVm) + if err != nil { + return mvm, err + } + + m := swingsetkeeper.NewMigrator(app.SwingSetKeeper) + err = m.MigrateParams(ctx) + if err != nil { + return mvm, err + } + + return mvm, nil + } +} diff --git a/golang/cosmos/vm/core_proposals.go b/golang/cosmos/vm/core_proposals.go index 44aa9bd83f91..65d459f22d48 100644 --- a/golang/cosmos/vm/core_proposals.go +++ b/golang/cosmos/vm/core_proposals.go @@ -1,5 +1,10 @@ package vm +import ( + "encoding/json" + "fmt" +) + // CoreProposalStep is a set of core proposal configs which are executed // concurrently type CoreProposalStep []Jsonable @@ -11,12 +16,27 @@ type CoreProposals struct { Steps []CoreProposalStep `json:"steps"` } +type ArbitraryCoreProposal struct { + Json json.RawMessage +} + +func NewArbitraryCoreProposal(jsonStr string) *ArbitraryCoreProposal { + return &ArbitraryCoreProposal{Json: []byte(jsonStr)} +} + // CoreProposalStepForModules generates a single core proposal step from // the given modules, which will be executed concurrently during that step -func CoreProposalStepForModules(modules ...string) CoreProposalStep { +func CoreProposalStepForModules(modules ...Jsonable) CoreProposalStep { step := make([]Jsonable, len(modules)) for i := range modules { - step[i] = modules[i] + switch m := modules[i].(type) { + case ArbitraryCoreProposal: + step[i] = m.Json + case string: + step[i] = m + default: + panic(fmt.Errorf("unexpected step type %T", m)) + } } return step } diff --git a/packages/builders/scripts/vats/priceFeedSupport.js b/packages/builders/scripts/vats/priceFeedSupport.js index 309d4f0a8218..5f916d3c7931 100644 --- a/packages/builders/scripts/vats/priceFeedSupport.js +++ b/packages/builders/scripts/vats/priceFeedSupport.js @@ -2,41 +2,33 @@ import { DEFAULT_CONTRACT_TERMS } from '../inter-protocol/price-feed-core.js'; -const ORACLE_ADDRESSES = [ - // XXX These are the oracle addresses. They must be provided before the chain - // is running, which means they must be known ahead of time. - // see https://github.com/Agoric/agoric-3-proposals/issues/5 - 'agoric1lu9hh5vgx05hmlpfu47hukershgdxctk6l5s05', - 'agoric15lpnq2mjsdhtztf6khp7mrsq66hyrssspy92pd', - 'agoric1mwm224epc4l3pjcz7qsxnudcuktpynwkmnfqfp', -]; +const { Fail } = assert; /** * modified copy of ../inter-protocol/price-feed-core.js * * @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} */ -export const priceFeedProposalBuilder = async ( +export const strictPriceFeedProposalBuilder = async ( { publishRef, install }, - options = {}, + options, ) => { const { AGORIC_INSTANCE_NAME, IN_BRAND_LOOKUP, IN_BRAND_NAME = IN_BRAND_LOOKUP[IN_BRAND_LOOKUP.length - 1], + ORACLE_ADDRESSES, } = options; - const { GOV1ADDR, GOV2ADDR, GOV3ADDR } = process.env; - const oracleAddresses = - GOV1ADDR || GOV2ADDR || GOV3ADDR - ? [GOV1ADDR, GOV2ADDR, GOV3ADDR].filter(x => x) - : ORACLE_ADDRESSES; - assert(Array.isArray(oracleAddresses), 'oracleAddresses array is required'); + const oracleAddresses = ORACLE_ADDRESSES; + Array.isArray(oracleAddresses) || + Fail`ORACLE_ADDRESSES array is required; got ${oracleAddresses}`; - assert(AGORIC_INSTANCE_NAME, 'AGORIC_INSTANCE_NAME is required'); + AGORIC_INSTANCE_NAME || + Fail`AGORIC_INSTANCE_NAME is required; got ${AGORIC_INSTANCE_NAME}`; - assert.equal(IN_BRAND_LOOKUP[0], 'agoricNames'); - assert(IN_BRAND_NAME, 'brandIn is required'); + Array.isArray(IN_BRAND_LOOKUP) || + Fail`IN_BRAND_NAME array is required; got ${IN_BRAND_LOOKUP}`; return harden({ sourceSpec: '@agoric/inter-protocol/src/proposals/price-feed-proposal.js', @@ -60,3 +52,39 @@ export const priceFeedProposalBuilder = async ( ], }); }; + +/** + * @deprecated use `strictPriceFeedProposalBuilder` and specify arguments instead + * @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} + */ +export const deprecatedPriceFeedProposalBuilder = async (powers, options) => { + console.warn( + 'deprecated ambient `priceFeedProposalBuilder`; use `strictPriceFeedProposalBuilder` instead', + ); + + const DEFAULT_ORACLE_ADDRESSES = [ + // XXX These are the oracle addresses. They must be provided before the chain + // is running, which means they must be known ahead of time. + // see https://github.com/Agoric/agoric-3-proposals/issues/5 + 'agoric1lu9hh5vgx05hmlpfu47hukershgdxctk6l5s05', + 'agoric15lpnq2mjsdhtztf6khp7mrsq66hyrssspy92pd', + 'agoric1mwm224epc4l3pjcz7qsxnudcuktpynwkmnfqfp', + ]; + + const { GOV1ADDR, GOV2ADDR, GOV3ADDR } = process.env; + const governanceAddressEnv = [GOV1ADDR, GOV2ADDR, GOV3ADDR].filter(x => x); + const ORACLE_ADDRESSES = governanceAddressEnv.length + ? governanceAddressEnv + : DEFAULT_ORACLE_ADDRESSES; + + return strictPriceFeedProposalBuilder(powers, { + ...options, + ORACLE_ADDRESSES, + }); +}; + +/** + * @deprecated use `strictPriceFeedProposalBuilder` and specify arguments instead + * @type {import('@agoric/deploy-script-support/src/externalTypes.js').CoreEvalBuilder} + */ +export const priceFeedProposalBuilder = deprecatedPriceFeedProposalBuilder; diff --git a/packages/builders/scripts/vats/updateStAtomPriceFeed.js b/packages/builders/scripts/vats/updateStAtomPriceFeed.js index 6d3c226b30e5..456c1b73fff1 100644 --- a/packages/builders/scripts/vats/updateStAtomPriceFeed.js +++ b/packages/builders/scripts/vats/updateStAtomPriceFeed.js @@ -3,7 +3,7 @@ import { priceFeedProposalBuilder } from './priceFeedSupport.js'; const OPTIONS = { AGORIC_INSTANCE_NAME: 'stATOM-USD price feed', - IN_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'stAtom'], + IN_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'stATOM'], OUT_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'USD'], }; diff --git a/packages/builders/scripts/vats/updateStkAtomPriceFeed.js b/packages/builders/scripts/vats/updateStkAtomPriceFeed.js index 2fd2a671f1e8..1226b7e201f9 100644 --- a/packages/builders/scripts/vats/updateStkAtomPriceFeed.js +++ b/packages/builders/scripts/vats/updateStkAtomPriceFeed.js @@ -3,7 +3,7 @@ import { priceFeedProposalBuilder } from './priceFeedSupport.js'; const OPTIONS = { AGORIC_INSTANCE_NAME: 'stkATOM-USD price feed', - IN_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'stkAtom'], + IN_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'stkATOM'], OUT_BRAND_LOOKUP: ['agoricNames', 'oracleBrand', 'USD'], }; diff --git a/packages/inter-protocol/src/proposals/upgrade-vaults.js b/packages/inter-protocol/src/proposals/upgrade-vaults.js index 0c7ee64caaf9..7c8547bf2c62 100644 --- a/packages/inter-protocol/src/proposals/upgrade-vaults.js +++ b/packages/inter-protocol/src/proposals/upgrade-vaults.js @@ -2,23 +2,29 @@ import { E } from '@endo/far'; import { makeNotifierFromAsyncIterable } from '@agoric/notifier'; import { AmountMath } from '@agoric/ertp/src/index.js'; import { makeTracer } from '@agoric/internal/src/index.js'; +import { isUpgradeDisconnection } from '@agoric/internal/src/upgrade-api.js'; const trace = makeTracer('upgrade Vaults proposal'); // stand-in for Promise.any() which isn't available at this point. +/** @param {Promise[]} promises */ const any = promises => new Promise((resolve, reject) => { for (const promise of promises) { - promise.then(resolve); + void promise.then(resolve, () => {}); } void Promise.allSettled(promises).then(results => { - const rejects = results.filter(({ status }) => status === 'rejected'); + const rejects = /** @type {PromiseRejectedResult[]} */ ( + results.filter(({ status }) => status === 'rejected') + ); if (rejects.length === results.length) { - // @ts-expect-error TypeScript doesn't know enough - const messages = rejects.map(({ message }) => message); + const messages = rejects.map( + ({ reason: { message } }) => message || 'no error message', + ); const aggregate = new Error(messages.join(';')); - // @ts-expect-error TypeScript doesn't know enough - aggregate.errors = rejects.map(({ reason }) => reason); + /** @type {any} */ (aggregate).errors = rejects.map( + ({ reason }) => reason, + ); reject(aggregate); } }); @@ -139,15 +145,34 @@ export const upgradeVaults = async (powers, { options }) => { newPrivateArgs, ); - console.log('upgraded vaultFactory.', upgradeResult); + trace('upgraded vaultFactory.', upgradeResult); }; // Wait for at least one new price feed to be ready before upgrading Vaults void E.when( any( - Object.values(vaultBrands).map(brand => - E(priceAuthority).quoteGiven(AmountMath.make(brand, 10n), istBrand), - ), + Object.values(vaultBrands).map(async brand => { + const getQuote = async lastRejectionReason => { + await null; + try { + return await E(priceAuthority).quoteGiven( + AmountMath.make(brand, 10n), + istBrand, + ); + } catch (reason) { + if ( + isUpgradeDisconnection(reason) && + (!lastRejectionReason || + reason.incarnationNumber > + lastRejectionReason.incarnationNumber) + ) { + return getQuote(reason); + } + throw reason; + } + }; + return getQuote(null); + }), ), async price => { trace(`upgrading after delay`, price); @@ -165,6 +190,14 @@ export const upgradeVaults = async (powers, { options }) => { // @ts-expect-error auctioneerKit is non-null except between auctioneerKitProducer.reset() and auctioneerKitProducer.resolve() auctioneerKit.instance, ); + trace(`upgrading complete`, price); + }, + error => { + console.error( + 'Failed to upgrade vaultFactory', + error.message, + ...(error.errors || []), + ); }, );