feat: misc improvements to infra deployment (#4374)

feat: misc improvements to infra deployments

These are all things that I've done somewhat ad-hoc before, but now
upstreaming into the monorepo.

- write the deploy plan to a file instead of stdout
	- to avoid cluttering terminal with 1000+ line configs
- `getMultiProviderForRole` should only be populated with the requested
set of chains or the supported chains for given environmen
	- to avoid fetching 50+ more keys than you need to on any given deploy
- remove the agent config write-back from the post-deploy
- this is already covered by the update-agent-config.ts script which is
usually ran manually anyway
- also moved the helper function into the update-agent-config.ts file,
as that is the only place it is being used right now
	- in `infra/scripts` instead of `infra/src` to avoid a dependency issue
- add `writeYamlAtPath` helper method to utils
	- with small refactor of existing `writeJsonAtPath`
- streamline hit detection on `readCache` in legacy deployer
- improve logging of legacy hook deployer when deploying an aggregation
hook
- better config matching detection when deploying routing hooks
- extension of
https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4305

---------

Signed-off-by: pbio <10051819+paulbalaji@users.noreply.github.com>
pull/4378/head
Paul Balaji 3 months ago committed by GitHub
parent 1094ddfa91
commit e51cd1d438
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      rust/config/mainnet_config.json
  2. 7
      rust/utils/run-locally/src/ethereum/mod.rs
  3. 1
      typescript/infra/.gitignore
  4. 2
      typescript/infra/config/environments/mainnet3/helloworld.ts
  5. 4
      typescript/infra/config/environments/mainnet3/index.ts
  6. 3
      typescript/infra/config/environments/testnet4/index.ts
  7. 5
      typescript/infra/package.json
  8. 24
      typescript/infra/scripts/agent-utils.ts
  9. 111
      typescript/infra/scripts/agents/update-agent-config.ts
  10. 24
      typescript/infra/scripts/deploy.ts
  11. 1
      typescript/infra/src/config/environment.ts
  12. 119
      typescript/infra/src/deployment/deploy.ts
  13. 21
      typescript/infra/src/utils/utils.ts
  14. 19
      typescript/sdk/src/deploy/HyperlaneDeployer.ts
  15. 17
      typescript/sdk/src/hook/HyperlaneHookDeployer.ts

@ -1179,8 +1179,7 @@
},
"injective": {
"bech32Prefix": "inj",
"blockExplorers": [
],
"blockExplorers": [],
"blocks": {
"confirmations": 1,
"estimateBlockTime": 1,

@ -48,6 +48,13 @@ pub fn start_anvil(config: Arc<Config>) -> AgentHandles {
log!("Deploying hyperlane core contracts...");
yarn_infra.clone().cmd("deploy-core").run().join();
log!("Updating agent config...");
yarn_infra
.clone()
.cmd("update-agent-config:test")
.run()
.join();
log!("Deploying multicall contract...");
tokio::runtime::Builder::new_current_thread()
.enable_all()

@ -3,6 +3,7 @@ tmp.ts
dist/
.env*
cache/
deployment-plan.yaml
test/outputs
config/environments/test/core/
config/environments/test/igp/

@ -4,7 +4,7 @@ import {
} from '../../../src/config/helloworld/types.js';
import { Contexts } from '../../contexts.js';
import { environment, ethereumChainNames } from './chains.js';
import { environment } from './chains.js';
import hyperlaneAddresses from './helloworld/hyperlane/addresses.json';
import rcAddresses from './helloworld/rc/addresses.json';

@ -1,3 +1,5 @@
import { ChainName } from '@hyperlane-xyz/sdk';
import {
getKeysForRole,
getMultiProtocolProvider,
@ -28,9 +30,11 @@ export const environment: EnvironmentConfig = {
context: Contexts = Contexts.Hyperlane,
role: Role = Role.Deployer,
useSecrets?: boolean,
chains?: ChainName[],
) =>
getMultiProviderForRole(
environmentName,
chains && chains.length > 0 ? chains : supportedChainNames,
await getRegistry(useSecrets),
context,
role,

@ -1,4 +1,5 @@
import { IRegistry } from '@hyperlane-xyz/registry';
import { ChainName } from '@hyperlane-xyz/sdk';
import {
getKeysForRole,
@ -43,9 +44,11 @@ export const environment: EnvironmentConfig = {
context: Contexts = Contexts.Hyperlane,
role: Role = Role.Deployer,
useSecrets?: boolean,
chains?: ChainName[],
) =>
getMultiProviderForRole(
environmentName,
chains && chains.length > 0 ? chains : supportedChainNames,
await getRegistry(useSecrets),
context,
role,

@ -75,7 +75,10 @@
"test": "yarn test:unit && yarn test:hardhat",
"test:unit": "mocha --config ../sdk/.mocharc.json test/**/*.test.ts",
"test:hardhat": "yarn hardhat-esm test test/govern.hardhat-test.ts",
"test:ci": "yarn test"
"test:ci": "yarn test",
"update-agent-config:mainnet3": "tsx scripts/agents/update-agent-config.ts -e mainnet3",
"update-agent-config:test": "tsx scripts/agents/update-agent-config.ts -e test",
"update-agent-config:testnet4": "tsx scripts/agents/update-agent-config.ts -e testnet4"
},
"peerDependencies": {
"@ethersproject/abi": "*"

@ -8,6 +8,7 @@ import {
} from '@hyperlane-xyz/registry';
import {
ChainMap,
ChainMetadata,
ChainName,
CoreConfig,
MultiProtocolProvider,
@ -364,6 +365,7 @@ export async function getMultiProtocolProvider(
export async function getMultiProviderForRole(
environment: DeployEnvironment,
supportedChainNames: ChainName[],
registry: IRegistry,
context: Contexts,
role: Role,
@ -377,13 +379,21 @@ export async function getMultiProviderForRole(
return multiProvider;
}
await promiseObjAll(
objMap(chainMetadata, async (chain, _) => {
if (multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const key = getKeyForRole(environment, context, role, chain, index);
const signer = await key.getSigner();
multiProvider.setSigner(chain, signer);
}
}),
objMap(
supportedChainNames.reduce((acc, chain) => {
if (chainMetadata[chain]) {
acc[chain] = chainMetadata[chain];
}
return acc;
}, {} as ChainMap<ChainMetadata>),
async (chain, _) => {
if (multiProvider.getProtocol(chain) === ProtocolType.Ethereum) {
const key = getKeyForRole(environment, context, role, chain, index);
const signer = await key.getSigner();
multiProvider.setSigner(chain, signer);
}
},
),
);
return multiProvider;

@ -1,19 +1,114 @@
import { writeAgentConfig } from '../../src/deployment/deploy.js';
import { getArgs } from '../agent-utils.js';
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
ChainMap,
HyperlaneCore,
HyperlaneDeploymentArtifacts,
MultiProvider,
buildAgentConfig,
} from '@hyperlane-xyz/sdk';
import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts.js';
import {
DeployEnvironment,
envNameToAgentEnv,
} from '../../src/config/environment.js';
import { getCosmosChainGasPrice } from '../../src/config/gas-oracle.js';
import {
chainIsProtocol,
filterRemoteDomainMetadata,
writeMergedJSONAtPath,
} from '../../src/utils/utils.js';
import {
Modules,
getAddresses,
getAgentConfig,
getAgentConfigJsonPath,
getArgs,
} from '../agent-utils.js';
import { getEnvironmentConfig } from '../core-utils.js';
async function main() {
const { environment } = await getArgs().argv;
const envConfig = getEnvironmentConfig(environment);
const multiProvider = await envConfig.getMultiProvider();
await writeAgentConfig(multiProvider, environment);
}
// Keep as a function in case we want to use it in the future
export async function writeAgentConfig(
multiProvider: MultiProvider,
environment: DeployEnvironment,
) {
// Get the addresses for the environment
const addressesMap = getAddresses(
environment,
Modules.CORE,
) as ChainMap<ChainAddresses>;
const addressesForEnv = filterRemoteDomainMetadata(addressesMap);
const core = HyperlaneCore.fromAddressesMap(addressesForEnv, multiProvider);
// Write agent config indexing from the deployed Mailbox which stores the block number at deployment
const startBlocks = await promiseObjAll(
objMap(addressesForEnv, async (chain: string, _) => {
// If the index.from is specified in the chain metadata, use that.
const indexFrom = multiProvider.getChainMetadata(chain).index?.from;
if (indexFrom !== undefined) {
return indexFrom;
}
let multiProvider = await envConfig.getMultiProvider(
undefined,
undefined,
// Don't use secrets
false,
const mailbox = core.getContracts(chain).mailbox;
try {
const deployedBlock = await mailbox.deployedBlock();
return deployedBlock.toNumber();
} catch (err) {
console.error(
'Failed to get deployed block, defaulting to 0. Chain:',
chain,
'Error:',
err,
);
return 0;
}
}),
);
await writeAgentConfig(multiProvider, environment);
// Get gas prices for Cosmos chains.
// Instead of iterating through `addresses`, which only includes EVM chains,
// iterate through the environment chain names.
const envAgentConfig = getAgentConfig(Contexts.Hyperlane, environment);
const environmentChains = envAgentConfig.environmentChainNames;
const additionalConfig = Object.fromEntries(
await Promise.all(
environmentChains
.filter((chain) => chainIsProtocol(chain, ProtocolType.Cosmos))
.map(async (chain) => [
chain,
{
gasPrice: await getCosmosChainGasPrice(chain),
},
]),
),
);
const agentConfig = buildAgentConfig(
environmentChains,
await getEnvironmentConfig(environment).getMultiProvider(
undefined,
undefined,
// Don't use secrets
false,
),
addressesForEnv as ChainMap<HyperlaneDeploymentArtifacts>,
startBlocks,
additionalConfig,
);
writeMergedJSONAtPath(
getAgentConfigJsonPath(envNameToAgentEnv[environment]),
agentConfig,
);
}
main()

@ -34,7 +34,7 @@ import {
fetchExplorerApiKeys,
} from '../src/deployment/verify.js';
import { impersonateAccount, useLocalProvider } from '../src/utils/fork.js';
import { inCIMode } from '../src/utils/utils.js';
import { inCIMode, writeYamlAtPath } from '../src/utils/utils.js';
import {
Modules,
@ -70,7 +70,12 @@ async function main() {
).argv;
const envConfig = getEnvironmentConfig(environment);
let multiProvider = await envConfig.getMultiProvider();
let multiProvider = await envConfig.getMultiProvider(
undefined,
undefined,
undefined,
chains,
);
if (fork) {
multiProvider = multiProvider.extendChainMetadata({
@ -230,14 +235,6 @@ async function main() {
environment,
module,
};
// Don't write agent config in fork tests
const agentConfig =
module === Modules.CORE && !fork
? {
environment,
multiProvider,
}
: undefined;
// prompt for confirmation in production environments
if (environment !== 'test' && !fork) {
@ -247,7 +244,11 @@ async function main() {
(chains ?? []).includes(chain),
)
: config;
console.log(JSON.stringify(confirmConfig, null, 2));
const deployPlanPath = path.join(modulePath, 'deployment-plan.yaml');
writeYamlAtPath(deployPlanPath, confirmConfig);
console.log(`Deployment Plan written to ${deployPlanPath}`);
const { value: confirmed } = await prompts({
type: 'confirm',
name: 'value',
@ -266,7 +267,6 @@ async function main() {
// Use chains if provided, otherwise deploy to all chains
// If fork is provided, deploy to fork only
targetNetworks: chains && chains.length > 0 ? chains : !fork ? [] : [fork],
agentConfig,
});
}

@ -53,6 +53,7 @@ export type EnvironmentConfig = {
context?: Contexts,
role?: Role,
useSecrets?: boolean,
chains?: ChainName[],
) => Promise<MultiProvider>;
getKeys: (
context?: Contexts,

@ -1,47 +1,24 @@
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
ChainMap,
ChainName,
HyperlaneCore,
HyperlaneDeployer,
HyperlaneDeploymentArtifacts,
MultiProvider,
buildAgentConfig,
serializeContractsMap,
} from '@hyperlane-xyz/sdk';
import {
ProtocolType,
objFilter,
objMap,
objMerge,
promiseObjAll,
} from '@hyperlane-xyz/utils';
import { objFilter, objMerge } from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts.js';
import {
Modules,
getAddresses,
getAgentConfig,
getAgentConfigJsonPath,
writeAddresses,
} from '../../scripts/agent-utils.js';
import { getEnvironmentConfig } from '../../scripts/core-utils.js';
import { DeployEnvironment, envNameToAgentEnv } from '../config/environment.js';
import { getCosmosChainGasPrice } from '../config/gas-oracle.js';
import {
chainIsProtocol,
filterRemoteDomainMetadata,
readJSONAtPath,
writeJsonAtPath,
writeMergedJSONAtPath,
} from '../utils/utils.js';
import { DeployEnvironment } from '../config/environment.js';
import { readJSONAtPath, writeJsonAtPath } from '../utils/utils.js';
export async function deployWithArtifacts<Config extends object>({
configMap,
deployer,
cache,
targetNetworks,
agentConfig,
}: {
configMap: ChainMap<Config>;
deployer: HyperlaneDeployer<Config, any>;
@ -53,10 +30,6 @@ export async function deployWithArtifacts<Config extends object>({
module: Modules;
};
targetNetworks: ChainName[];
agentConfig?: {
multiProvider: MultiProvider;
environment: DeployEnvironment;
};
}) {
if (cache.read) {
const addressesMap = getAddresses(cache.environment, cache.module);
@ -65,7 +38,7 @@ export async function deployWithArtifacts<Config extends object>({
process.on('SIGINT', async () => {
// Call the post deploy hook to write the addresses and verification
await postDeploy(deployer, cache, agentConfig);
await postDeploy(deployer, cache);
console.log('\nCaught (Ctrl+C), gracefully exiting...');
process.exit(0); // Exit the process
@ -90,7 +63,7 @@ export async function deployWithArtifacts<Config extends object>({
}
}
await postDeploy(deployer, cache, agentConfig);
await postDeploy(deployer, cache);
}
export async function postDeploy<Config extends object>(
@ -102,10 +75,6 @@ export async function postDeploy<Config extends object>(
environment: DeployEnvironment;
module: Modules;
},
agentConfig?: {
multiProvider: MultiProvider;
environment: DeployEnvironment;
},
) {
if (cache.write) {
// TODO: dedupe deployedContracts with cachedAddresses
@ -128,82 +97,4 @@ export async function postDeploy<Config extends object>(
deployer.mergeWithExistingVerificationInputs(savedVerification);
writeJsonAtPath(cache.verification, inputs);
}
if (agentConfig) {
await writeAgentConfig(agentConfig.multiProvider, agentConfig.environment);
}
}
export async function writeAgentConfig(
multiProvider: MultiProvider,
environment: DeployEnvironment,
) {
// Get the addresses for the environment
const addressesMap = getAddresses(
environment,
Modules.CORE,
) as ChainMap<ChainAddresses>;
const addressesForEnv = filterRemoteDomainMetadata(addressesMap);
const core = HyperlaneCore.fromAddressesMap(addressesForEnv, multiProvider);
// Write agent config indexing from the deployed Mailbox which stores the block number at deployment
const startBlocks = await promiseObjAll(
objMap(addressesForEnv, async (chain: string, _) => {
// If the index.from is specified in the chain metadata, use that.
const indexFrom = multiProvider.getChainMetadata(chain).index?.from;
if (indexFrom !== undefined) {
return indexFrom;
}
const mailbox = core.getContracts(chain).mailbox;
try {
const deployedBlock = await mailbox.deployedBlock();
return deployedBlock.toNumber();
} catch (err) {
console.error(
'Failed to get deployed block, defaulting to 0. Chain:',
chain,
'Error:',
err,
);
return 0;
}
}),
);
// Get gas prices for Cosmos chains.
// Instead of iterating through `addresses`, which only includes EVM chains,
// iterate through the environment chain names.
const envAgentConfig = getAgentConfig(Contexts.Hyperlane, environment);
const environmentChains = envAgentConfig.environmentChainNames;
const additionalConfig = Object.fromEntries(
await Promise.all(
environmentChains
.filter((chain) => chainIsProtocol(chain, ProtocolType.Cosmos))
.map(async (chain) => [
chain,
{
gasPrice: await getCosmosChainGasPrice(chain),
},
]),
),
);
const agentConfig = buildAgentConfig(
environmentChains,
await getEnvironmentConfig(environment).getMultiProvider(
undefined,
undefined,
// Don't use secrets
false,
),
addressesForEnv as ChainMap<HyperlaneDeploymentArtifacts>,
startBlocks,
additionalConfig,
);
writeMergedJSONAtPath(
getAgentConfigJsonPath(envNameToAgentEnv[environment]),
agentConfig,
);
}

@ -2,8 +2,8 @@
import asn1 from 'asn1.js';
import { exec } from 'child_process';
import { ethers } from 'ethers';
// eslint-disable-next-line
import fs from 'fs';
import stringify from 'json-stable-stringify';
import path, { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { parse as yamlParse } from 'yaml';
@ -14,6 +14,7 @@ import {
ProtocolType,
objFilter,
objMerge,
stringifyObject,
} from '@hyperlane-xyz/utils';
import { Contexts } from '../../config/contexts.js';
@ -150,12 +151,26 @@ export function writeMergedJSON(directory: string, filename: string, obj: any) {
writeMergedJSONAtPath(path.join(directory, filename), obj);
}
export function writeJsonAtPath(filepath: string, obj: any) {
function ensureDirectoryExists(filepath: string) {
const dir = path.dirname(filepath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filepath, stringify(obj, { space: ' ' }) + '\n');
}
function writeToFile(filepath: string, content: string) {
ensureDirectoryExists(filepath);
fs.writeFileSync(filepath, content + '\n');
}
export function writeJsonAtPath(filepath: string, obj: any) {
const content = stringifyObject(obj, 'json', 2);
writeToFile(filepath, content);
}
export function writeYamlAtPath(filepath: string, obj: any) {
const content = stringifyObject(obj, 'yaml');
writeToFile(filepath, content);
}
export function writeJSON(directory: string, filename: string, obj: any) {

@ -16,6 +16,7 @@ import {
Address,
ProtocolType,
eqAddress,
isZeroishAddress,
rootLogger,
runWithTimeout,
} from '@hyperlane-xyz/utils';
@ -637,19 +638,15 @@ export abstract class HyperlaneDeployer<
contractName: string,
): Awaited<ReturnType<F['deploy']>> | undefined {
const cachedAddress = this.cachedAddresses[chain]?.[contractName];
const hit =
!!cachedAddress && cachedAddress !== ethers.constants.AddressZero;
const contractAddress = hit ? cachedAddress : ethers.constants.AddressZero;
const contract = factory
.attach(contractAddress)
.connect(this.multiProvider.getSignerOrProvider(chain)) as Awaited<
ReturnType<F['deploy']>
>;
if (hit) {
if (cachedAddress && !isZeroishAddress(cachedAddress)) {
this.logger.debug(
`Recovered ${contractName.toString()} on ${chain} ${cachedAddress}`,
`Recovered ${contractName} on ${chain}: ${cachedAddress}`,
);
return contract;
return factory
.attach(cachedAddress)
.connect(this.multiProvider.getSignerOrProvider(chain)) as Awaited<
ReturnType<F['deploy']>
>;
}
return undefined;
}

@ -9,7 +9,12 @@ import {
ProtocolFee,
StaticAggregationHook__factory,
} from '@hyperlane-xyz/core';
import { Address, addressToBytes32, rootLogger } from '@hyperlane-xyz/utils';
import {
Address,
addressToBytes32,
deepEquals,
rootLogger,
} from '@hyperlane-xyz/utils';
import { HyperlaneContracts } from '../contracts/types.js';
import { CoreAddresses } from '../core/contracts.js';
@ -166,7 +171,13 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
aggregatedHooks.push(subhooks[hookConfig.type].address);
hooks = { ...hooks, ...subhooks };
}
this.logger.debug(`Deploying aggregation hook of ${config.hooks}`);
this.logger.debug(
{ aggregationHook: config.hooks },
`Deploying aggregation hook of type ${config.hooks.map((h) =>
typeof h === 'string' ? h : h.type,
)}...`,
);
const address = await this.ismFactory.deployStaticAddressSet(
chain,
this.ismFactory.getContracts(chain).staticAggregationHookFactory,
@ -309,7 +320,7 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
this.logger.debug(`Deploying routing hook for ${dest}`);
const destDomain = this.multiProvider.getDomainId(dest);
if (prevHookConfig && prevHookAddress && prevHookConfig === hookConfig) {
if (deepEquals(prevHookConfig, hookConfig) && prevHookAddress) {
this.logger.debug(`Reusing hook ${prevHookAddress} for ${dest}`);
routingConfigs.push({
destination: destDomain,

Loading…
Cancel
Save