feat:allow full IsmConfig for CLI as an advanced config feature (#2939)

### Description

- added `ism-advanced` config which allows you flexibility to define
your own IsmConfig shape
  - configure through `hyperlane config create ism-advanced`
- core deployments checks for both `ism-advanced.yaml` and then
`ism.yaml` (old multisig config way) ie. precedence is given to the
advanced version
- support for `Aggregation`, `Routing`, `MerkleRoot`, `MessageId`, and
`TestIsm` (with a warning)
- instead of routing over just messageId, the simpler `ism` config way
is now routing over aggregation of merkle and message matching hyperlane
V3 core deployments
- also renamed multisig option to ism to not introduce multisig
user-side and stick with consistent ism or ism- options.

### Drive-by changes

- `ism.yaml` doesn't take in a type since we provide both `merkleRoot`
and `messageId` variants by default.

### Related issues

- fixes https://github.com/hyperlane-xyz/issues/issues/737

### Backward compatibility

Yes

### Testing

Manual

---------

Co-authored-by: Yorke Rhodes <yorke@hyperlane.xyz>
pull/2990/head
Kunal Arora 12 months ago committed by GitHub
parent 50aed86517
commit df693708b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .changeset/tiny-teachers-accept.md
  2. 2
      typescript/cli/ci-test.sh
  3. 33
      typescript/cli/examples/ism-advanced.yaml
  4. 8
      typescript/cli/examples/ism.yaml
  5. 53
      typescript/cli/src/commands/config.ts
  6. 2
      typescript/cli/src/commands/deploy.ts
  7. 4
      typescript/cli/src/config/hooks.ts
  8. 268
      typescript/cli/src/config/ism.ts
  9. 46
      typescript/cli/src/config/multisig.ts
  10. 119
      typescript/cli/src/deploy/core.ts
  11. 21
      typescript/cli/src/deploy/utils.ts
  12. 85
      typescript/cli/src/tests/ism.test.ts
  13. 33
      typescript/cli/src/tests/ism/routing-same-chain-fail.yaml
  14. 32
      typescript/cli/src/tests/ism/safe-parse-fail.yaml
  15. 33
      typescript/cli/src/tests/ism/threshold-gt-modules-length-fail.yaml
  16. 33
      typescript/cli/src/tests/ism/wrong-ism-type-fail.yaml
  17. 41
      typescript/cli/src/tests/multisig.test.ts
  18. 8
      typescript/cli/src/tests/multisig/invalid-address-fail.yaml
  19. 6
      typescript/cli/src/tests/multisig/safe-parse-fail.yaml
  20. 8
      typescript/cli/src/tests/multisig/threshold-gt-fail.yaml
  21. 13
      typescript/cli/src/utils/chains.ts
  22. 5
      typescript/sdk/src/index.ts
  23. 35
      typescript/sdk/src/ism/multisig.ts
  24. 24
      yarn.lock

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add support for all ISM types in CLI interactive config creation

@ -38,7 +38,7 @@ yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
--targets anvil1,anvil2 \
--chains ./examples/anvil-chains.yaml \
--artifacts /tmp/empty-artifacts.json \
--ism ./examples/multisig-ism.yaml \
--ism ./examples/ism.yaml \
--hook ./examples/hook-config.yaml \
--out /tmp \
--key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \

@ -0,0 +1,33 @@
anvil1:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil2:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1
anvil2:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil1:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1

@ -1,21 +1,13 @@
# A config for a multisig Interchain Security Module (ISM)
# Schema: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/ism/types.ts
#
# Valid module types:
# routing
# aggregation
# merkleRootMultisig
# messageIdMultisigIsm
# ism type don't work currently (sets to messageIdMultisigIsm for all)
---
anvil1:
type: 'messageIdMultisigIsm'
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
anvil2:
type: 'messageIdMultisigIsm'
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'

@ -3,6 +3,7 @@ import { CommandModule } from 'yargs';
import { log, logGreen } from '../../logger.js';
import { createChainConfig, readChainConfigs } from '../config/chain.js';
import { createHookConfig } from '../config/hooks.js';
import { createIsmConfigMap, readIsmConfig } from '../config/ism.js';
import {
createMultisigConfig,
readMultisigConfig,
@ -40,7 +41,7 @@ const createCommand: CommandModule = {
builder: (yargs) =>
yargs
.command(createChainConfigCommand)
.command(createMultisigConfigCommand)
.command(createIsmConfigCommand)
.command(createHookConfigCommand)
.command(createWarpConfigCommand)
.version(false)
@ -64,20 +65,32 @@ const createChainConfigCommand: CommandModule = {
},
};
const createMultisigConfigCommand: CommandModule = {
command: 'multisig',
describe: 'Create a new Multisig ISM config',
const createIsmConfigCommand: CommandModule = {
command: 'ism',
describe: 'Create a basic or advanced ISM config for a validator set',
builder: (yargs) =>
yargs.options({
output: outputFileOption('./configs/multisig-ism.yaml'),
output: outputFileOption('./configs/ism.yaml'),
format: fileFormatOption,
chains: chainsCommandOption,
advanced: {
type: 'boolean',
describe: 'Create an advanced ISM configuration',
default: false,
},
}),
handler: async (argv: any) => {
const format: FileFormat = argv.format;
const outPath: string = argv.output;
const chainConfigPath: string = argv.chains;
await createMultisigConfig({ format, outPath, chainConfigPath });
const isAdvanced: boolean = argv.advanced;
if (isAdvanced) {
await createIsmConfigMap({ format, outPath, chainConfigPath });
} else {
await createMultisigConfig({ format, outPath, chainConfigPath });
}
process.exit(0);
},
};
@ -127,7 +140,8 @@ const validateCommand: CommandModule = {
builder: (yargs) =>
yargs
.command(validateChainCommand)
.command(validateMultisigCommand)
.command(validateIsmCommand)
.command(validateIsmAdvancedCommand)
.command(validateWarpCommand)
.version(false)
.demandCommand(),
@ -152,9 +166,9 @@ const validateChainCommand: CommandModule = {
},
};
const validateMultisigCommand: CommandModule = {
command: 'multisig',
describe: 'Validate a multisig ism config in a YAML or JSON file',
const validateIsmCommand: CommandModule = {
command: 'ism',
describe: 'Validate the basic ISM config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
@ -171,6 +185,25 @@ const validateMultisigCommand: CommandModule = {
},
};
const validateIsmAdvancedCommand: CommandModule = {
command: 'ism-advanced',
describe: 'Validate the advanced ISM config in a YAML or JSON file',
builder: (yargs) =>
yargs.options({
path: {
type: 'string',
description: 'Input file path',
demandOption: true,
},
}),
handler: async (argv) => {
const path = argv.path as string;
readIsmConfig(path);
logGreen('Config is valid');
process.exit(0);
},
};
const validateWarpCommand: CommandModule = {
command: 'warp',
describe: 'Validate a Warp Route config in a YAML or JSON file',

@ -81,7 +81,7 @@ const coreCommand: CommandModule = {
ism: {
type: 'string',
description:
'A path to a JSON or YAML file with ISM configs (e.g. Multisig)',
'A path to a JSON or YAML file with basic or advanced ISM configs (e.g. Multisig)',
},
hook: {
type: 'string',

@ -116,7 +116,7 @@ export function presetHookConfigs(
export function readHookConfig(filePath: string) {
const config = readYamlOrJson(filePath);
if (!config) {
logRed(`No multisig config found at ${filePath}`);
logRed(`No hook config found at ${filePath}`);
return;
}
const result = HookConfigMapSchema.safeParse(config);
@ -134,7 +134,7 @@ export function readHookConfig(filePath: string) {
type: config.default.type,
} as HookConfig),
);
logGreen(`All multisig configs in ${filePath} are valid`);
logGreen(`All hook configs in ${filePath} are valid`);
return defaultHook;
}

@ -0,0 +1,268 @@
import { confirm, input, select } from '@inquirer/prompts';
import { z } from 'zod';
import { ChainMap, ChainName, IsmType } from '@hyperlane-xyz/sdk';
import { errorRed, log, logBlue, logGreen } from '../../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import { FileFormat, mergeYamlOrJson, readYamlOrJson } from '../utils/files.js';
import { readChainConfigsIfExists } from './chain.js';
const MultisigIsmConfigSchema = z.object({
type: z.union([
z.literal(IsmType.MERKLE_ROOT_MULTISIG),
z.literal(IsmType.MESSAGE_ID_MULTISIG),
]),
threshold: z.number(),
validators: z.array(z.string()),
});
const RoutingIsmConfigSchema: z.ZodSchema<any> = z.lazy(() =>
z.object({
type: z.literal(IsmType.ROUTING),
owner: z.string(),
domains: z.record(IsmConfigSchema),
}),
);
const AggregationIsmConfigSchema: z.ZodSchema<any> = z
.lazy(() =>
z.object({
type: z.literal(IsmType.AGGREGATION),
modules: z.array(IsmConfigSchema),
threshold: z.number(),
}),
)
.refine(
// check ig modules.length >= threshold
(ismConfig) => {
return ismConfig.modules.length >= ismConfig.threshold;
},
{
message: 'Threshold cannot be greater than number of modules',
},
);
const TestIsmConfigSchema = z.object({
type: z.literal(IsmType.TEST_ISM),
});
const IsmConfigSchema = z.union([
MultisigIsmConfigSchema,
RoutingIsmConfigSchema,
AggregationIsmConfigSchema,
TestIsmConfigSchema,
]);
const IsmConfigMapSchema = z.record(IsmConfigSchema).refine(
(ismConfigMap) => {
// check if any key in IsmConfigMap is found in its own RoutingIsmConfigSchema.domains
for (const [key, config] of Object.entries(ismConfigMap)) {
if (config.type === IsmType.ROUTING) {
if (config.domains && key in config.domains) {
return false;
}
}
}
return true;
},
{
message:
'Cannot set RoutingIsm.domain to the same chain you are configuring',
},
);
export type ZodIsmConfig = z.infer<typeof IsmConfigSchema>;
export type ZodIsmConfigMap = z.infer<typeof IsmConfigMapSchema>;
export function parseIsmConfig(filePath: string) {
const config = readYamlOrJson(filePath);
if (!config) throw new Error(`No ISM config found at ${filePath}`);
return IsmConfigMapSchema.safeParse(config);
}
export function readIsmConfig(filePath: string) {
const result = parseIsmConfig(filePath);
if (!result.success) {
const firstIssue = result.error.issues[0];
throw new Error(
`Invalid ISM config: ${firstIssue.path} => ${firstIssue.message}`,
);
}
const parsedConfig = result.data;
return parsedConfig;
}
export function isValildIsmConfig(config: any) {
return IsmConfigMapSchema.safeParse(config).success;
}
export async function createIsmConfigMap({
format,
outPath,
chainConfigPath,
}: {
format: FileFormat;
outPath: string;
chainConfigPath: string;
}) {
logBlue('Creating a new ISM config');
const customChains = readChainConfigsIfExists(chainConfigPath);
const chains = await runMultiChainSelectionStep(customChains);
const result: ZodIsmConfigMap = {};
for (const chain of chains) {
log(`Setting values for chain ${chain}`);
result[chain] = await createIsmConfig(chain, chainConfigPath);
// TODO consider re-enabling. Disabling based on feedback from @nambrot for now.
// repeat = await confirm({
// message: 'Use this same config for remaining chains?',
// });
}
if (isValildIsmConfig(result)) {
logGreen(`ISM config is valid, writing to file ${outPath}`);
mergeYamlOrJson(outPath, result, format);
} else {
errorRed(
`ISM config is invalid, please see https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/cli/examples/ism.yaml for an example`,
);
throw new Error('Invalid ISM config');
}
}
export async function createIsmConfig(
chain: ChainName,
chainConfigPath: string,
): Promise<ZodIsmConfig> {
let lastConfig: ZodIsmConfig;
const moduleType = await select({
message: 'Select ISM type',
choices: [
{
value: IsmType.MESSAGE_ID_MULTISIG,
name: IsmType.MESSAGE_ID_MULTISIG,
description: 'Validators need to sign just this messageId',
},
{
value: IsmType.MERKLE_ROOT_MULTISIG,
name: IsmType.MERKLE_ROOT_MULTISIG,
description:
'Validators need to sign the root of the merkle tree of all messages from origin chain',
},
{
value: IsmType.ROUTING,
name: IsmType.ROUTING,
description:
'Each origin chain can be verified by the specified ISM type via RoutingISM',
},
{
value: IsmType.AGGREGATION,
name: IsmType.AGGREGATION,
description:
'You can aggregate multiple ISMs into one ISM via AggregationISM',
},
{
value: IsmType.TEST_ISM,
name: IsmType.TEST_ISM,
description:
'ISM where you can deliver messages without any validation (WARNING: only for testing, do not use in production)',
},
],
pageSize: 10,
});
if (
moduleType === IsmType.MESSAGE_ID_MULTISIG ||
moduleType === IsmType.MERKLE_ROOT_MULTISIG
) {
lastConfig = await createMultisigConfig(moduleType);
} else if (moduleType === IsmType.ROUTING) {
lastConfig = await createRoutingConfig(chain, chainConfigPath);
} else if (moduleType === IsmType.AGGREGATION) {
lastConfig = await createAggregationConfig(chain, chainConfigPath);
} else if (moduleType === IsmType.TEST_ISM) {
lastConfig = { type: IsmType.TEST_ISM };
} else {
throw new Error(`Invalid ISM type: ${moduleType}}`);
}
return lastConfig;
}
export async function createMultisigConfig(
type: IsmType.MERKLE_ROOT_MULTISIG | IsmType.MESSAGE_ID_MULTISIG,
): Promise<ZodIsmConfig> {
const thresholdInput = await input({
message: 'Enter threshold of signers (number)',
});
const threshold = parseInt(thresholdInput, 10);
const validatorsInput = await input({
message: 'Enter validator addresses (comma separated list)',
});
const validators = validatorsInput.split(',').map((v) => v.trim());
return {
type,
threshold,
validators,
};
}
export async function createAggregationConfig(
chain: ChainName,
chainConfigPath: string,
): Promise<ZodIsmConfig> {
const isms = parseInt(
await input({
message: 'Enter the number of ISMs to aggregate (number)',
}),
10,
);
const threshold = parseInt(
await input({
message: 'Enter the threshold of ISMs to for verification (number)',
}),
10,
);
const modules: Array<ZodIsmConfig> = [];
for (let i = 0; i < isms; i++) {
modules.push(await createIsmConfig(chain, chainConfigPath));
}
return {
type: IsmType.AGGREGATION,
modules,
threshold,
};
}
export async function createRoutingConfig(
chain: ChainName,
chainConfigPath: string,
): Promise<ZodIsmConfig> {
const owner = await input({
message: 'Enter owner address',
});
const ownerAddress = owner;
const customChains = readChainConfigsIfExists(chainConfigPath);
delete customChains[chain];
const chains = await runMultiChainSelectionStep(
customChains,
`Select origin chains to be verified on ${chain}`,
[chain],
);
const domainsMap: ChainMap<ZodIsmConfig> = {};
for (const chain of chains) {
await confirm({
message: `You are about to configure ISM from source chain ${chain}. Continue?`,
});
const config = await createIsmConfig(chain, chainConfigPath);
domainsMap[chain] = config;
}
return {
type: IsmType.ROUTING,
owner: ownerAddress,
domains: domainsMap,
};
}

@ -1,8 +1,13 @@
import { input, select } from '@inquirer/prompts';
import { input } from '@inquirer/prompts';
import { z } from 'zod';
import { ChainMap, IsmType, MultisigConfig } from '@hyperlane-xyz/sdk';
import { objMap } from '@hyperlane-xyz/utils';
import { ChainMap, MultisigConfig } from '@hyperlane-xyz/sdk';
import {
Address,
isValidAddress,
normalizeAddressEvm,
objMap,
} from '@hyperlane-xyz/utils';
import { errorRed, log, logBlue, logGreen } from '../../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
@ -12,7 +17,6 @@ import { readChainConfigsIfExists } from './chain.js';
const MultisigConfigMapSchema = z.object({}).catchall(
z.object({
type: z.nativeEnum(IsmType),
threshold: z.number(),
validators: z.array(z.string()),
}),
@ -32,14 +36,24 @@ export function readMultisigConfig(filePath: string) {
const parsedConfig = result.data;
const formattedConfig: ChainMap<MultisigConfig> = objMap(
parsedConfig,
(_, config) =>
({
type: config.type as IsmType,
(_, config) => {
if (config.threshold > config.validators.length)
throw new Error(
'Threshold cannot be greater than number of validators',
);
if (config.threshold < 1)
throw new Error('Threshold must be greater than 0');
const validators: Address[] = [];
for (const v of config.validators) {
if (isValidAddress(v)) validators.push(normalizeAddressEvm(v));
else throw new Error(`Invalid address ${v}`);
}
return {
threshold: config.threshold,
validators: config.validators,
} as MultisigConfig),
validators: validators,
} as MultisigConfig;
},
);
logGreen(`All multisig configs in ${filePath} are valid`);
return formattedConfig;
}
@ -70,17 +84,6 @@ export async function createMultisigConfig({
result[chain] = lastConfig;
continue;
}
// TODO consider using default and not offering options here
const moduleType = await select({
message: 'Select multisig type',
choices: [
// { value: 'routing, name: 'routing' }, // TODO add support
// { value: 'aggregation, name: 'aggregation' }, // TODO add support
{ value: IsmType.MERKLE_ROOT_MULTISIG, name: 'merkle root multisig' },
{ value: IsmType.MESSAGE_ID_MULTISIG, name: 'message id multisig' },
],
pageSize: 5,
});
const thresholdInput = await input({
message: 'Enter threshold of signers (number)',
@ -92,7 +95,6 @@ export async function createMultisigConfig({
});
const validators = validatorsInput.split(',').map((v) => v.trim());
lastConfig = {
type: moduleType,
threshold,
validators,
};

@ -16,13 +16,14 @@ import {
HyperlaneIsmFactory,
HyperlaneProxyFactoryDeployer,
IgpConfig,
IsmConfig,
IsmType,
MultiProvider,
MultisigConfig,
RoutingIsmConfig,
agentStartBlocks,
buildAgentConfig,
buildMultisigIsmConfigs,
buildAggregationIsmConfigs,
defaultMultisigConfigs,
multisigIsmVerificationCost,
serializeContractsMap,
@ -32,6 +33,7 @@ import { Address, objFilter, objMerge } from '@hyperlane-xyz/utils';
import { log, logBlue, logGray, logGreen, logRed } from '../../logger.js';
import { readDeploymentArtifacts } from '../config/artifacts.js';
import { readHookConfig } from '../config/hooks.js';
import { readIsmConfig } from '../config/ism.js';
import { readMultisigConfig } from '../config/multisig.js';
import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js';
import {
@ -50,6 +52,7 @@ import {
TestRecipientConfig,
TestRecipientDeployer,
} from './TestRecipientDeployer.js';
import { isISMConfig, isZODISMConfig } from './utils.js';
import { runPreflightChecksForChains } from './utils.js';
export async function runCoreDeploy({
@ -83,7 +86,15 @@ export async function runCoreDeploy({
);
}
const artifacts = await runArtifactStep(chains, artifactsPath);
const multisigConfig = await runIsmStep(chains, ismConfigPath);
const result = await runIsmStep(chains, ismConfigPath);
// we can either specify the full ISM config or just the multisig config
const isAdvancedIsm = isISMConfig(result);
const ismConfigs = isAdvancedIsm
? (result as ChainMap<IsmConfig>)
: undefined;
const multisigConfigs = isAdvancedIsm
? defaultMultisigConfigs
: (result as ChainMap<MultisigConfig>);
// TODO re-enable when hook config is actually used
await runHookStep(chains, hookConfigPath);
@ -92,7 +103,8 @@ export async function runCoreDeploy({
signer,
multiProvider,
artifacts,
multisigConfig,
ismConfigs,
multisigConfigs,
outPath,
skipConfirmation,
};
@ -129,7 +141,11 @@ async function runArtifactStep(
const artifactChains = Object.keys(artifacts).filter((c) =>
selectedChains.includes(c),
);
log(`Found existing artifacts for chains: ${artifactChains.join(', ')}`);
if (artifactChains.length === 0) {
logGray('No artifacts found for selected chains');
} else {
log(`Found existing artifacts for chains: ${artifactChains.join(', ')}`);
}
return artifacts;
}
@ -140,8 +156,7 @@ async function runIsmStep(selectedChains: ChainName[], ismConfigPath?: string) {
'Hyperlane instances requires an Interchain Security Module (ISM).',
);
logGray(
'Note, only Multisig ISM configs are currently supported in the CLI',
'Example config: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/cli/typescript/cli/examples/multisig-ism.yaml',
'Example config: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/cli/typescript/cli/examples/ism.yaml',
);
ismConfigPath = await runFileSelectionStep(
'./configs',
@ -149,27 +164,53 @@ async function runIsmStep(selectedChains: ChainName[], ismConfigPath?: string) {
'ism',
);
}
// first we check for user provided chains
const multisigConfigs = {
...defaultMultisigConfigs,
...readMultisigConfig(ismConfigPath),
} as ChainMap<MultisigConfig>;
const requiredMultisigs = objFilter(
multisigConfigs,
(chain, config): config is MultisigConfig => selectedChains.includes(chain),
);
// selected chains - (user configs + default configs) = missing config
const missingConfigs = selectedChains.filter(
(c) => !Object.keys(requiredMultisigs).includes(c),
);
if (missingConfigs.length > 0) {
throw new Error(
`Missing ISM config for one or more chains: ${missingConfigs.join(', ')}`,
const isAdvancedIsm = isZODISMConfig(ismConfigPath);
// separate flow for 'ism' and 'ism-advanced' options
if (isAdvancedIsm) {
const ismConfig = readIsmConfig(ismConfigPath);
const requiredIsms = objFilter(
ismConfig,
(chain, config): config is IsmConfig => selectedChains.includes(chain),
);
}
// selected chains - (user configs + default configs) = missing config
const missingConfigs = selectedChains.filter(
(c) => !Object.keys(ismConfig).includes(c),
);
if (missingConfigs.length > 0) {
throw new Error(
`Missing advanced ISM config for one or more chains: ${missingConfigs.join(
', ',
)}`,
);
}
log(`Found configs for chains: ${selectedChains.join(', ')}`);
return requiredMultisigs;
log(`Found configs for chains: ${selectedChains.join(', ')}`);
return requiredIsms as ChainMap<IsmConfig>;
} else {
const multisigConfigs = {
...defaultMultisigConfigs,
...readMultisigConfig(ismConfigPath),
} as ChainMap<MultisigConfig>;
const requiredMultisigs = objFilter(
multisigConfigs,
(chain, config): config is MultisigConfig =>
selectedChains.includes(chain),
);
// selected chains - (user configs + default configs) = missing config
const missingConfigs = selectedChains.filter(
(c) => !Object.keys(requiredMultisigs).includes(c),
);
if (missingConfigs.length > 0) {
throw new Error(
`Missing ISM config for one or more chains: ${missingConfigs.join(
', ',
)}`,
);
}
log(`Found configs for chains: ${selectedChains.join(', ')}`);
return requiredMultisigs as ChainMap<MultisigConfig>;
}
}
async function runHookStep(
@ -201,7 +242,8 @@ interface DeployParams {
signer: ethers.Signer;
multiProvider: MultiProvider;
artifacts?: HyperlaneAddressesMap<any>;
multisigConfig?: ChainMap<MultisigConfig>;
ismConfigs?: ChainMap<IsmConfig>;
multisigConfigs?: ChainMap<MultisigConfig>;
outPath: string;
skipConfirmation: boolean;
}
@ -237,7 +279,8 @@ async function executeDeploy({
multiProvider,
outPath,
artifacts = {},
multisigConfig = {},
ismConfigs = {},
multisigConfigs = {},
}: DeployParams) {
logBlue('All systems ready, captain! Beginning deployment...');
@ -280,7 +323,9 @@ async function executeDeploy({
const defaultIsms: ChainMap<Address> = {};
for (const ismOrigin of chains) {
logBlue(`Deploying ISM to ${ismOrigin}`);
const ismConfig = buildIsmConfig(owner, ismOrigin, chains, multisigConfig);
const ismConfig =
ismConfigs[ismOrigin] ??
buildIsmConfig(owner, ismOrigin, chains, multisigConfigs);
ismContracts[ismOrigin] = {
multisigIsm: await ismFactory.deploy(ismOrigin, ismConfig),
};
@ -297,7 +342,7 @@ async function executeDeploy({
owner,
chains,
defaultIsms,
multisigConfig,
multisigConfigs ?? defaultMultisigConfigs, // TODO: fix https://github.com/hyperlane-xyz/issues/issues/773
);
const coreContracts = await coreDeployer.deploy(coreConfigs);
artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts);
@ -333,8 +378,7 @@ function buildIsmConfig(
chains: ChainName[],
multisigIsmConfigs: ChainMap<MultisigConfig>,
): RoutingIsmConfig {
const multisigConfigs = buildMultisigIsmConfigs(
IsmType.MESSAGE_ID_MULTISIG,
const aggregationIsmConfigs = buildAggregationIsmConfigs(
local,
chains,
multisigIsmConfigs,
@ -342,7 +386,7 @@ function buildIsmConfig(
return {
owner,
type: IsmType.ROUTING,
domains: multisigConfigs,
domains: aggregationIsmConfigs,
};
}
@ -410,9 +454,16 @@ function buildIgpConfigMap(
const gasOracleType: ChainMap<GasOracleContractType> = {};
for (const remote of chains) {
if (chain === remote) continue;
// TODO: accurate estimate of gas from ChainMap<ISMConfig>
const threshold = multisigConfigs[remote]
? multisigConfigs[remote].threshold
: 2;
const validatorsLength = multisigConfigs[remote]
? multisigConfigs[remote].validators.length
: 3;
overhead[remote] = multisigIsmVerificationCost(
multisigConfigs[chain].threshold,
multisigConfigs[chain].validators.length,
threshold,
validatorsLength,
);
gasOracleType[remote] = GasOracleContractType.StorageGasOracle;
}

@ -1,9 +1,16 @@
import { ethers } from 'ethers';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
import {
ChainMap,
ChainName,
IsmConfig,
MultiProvider,
MultisigConfig,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { log, logGreen } from '../../logger.js';
import { parseIsmConfig } from '../config/ism.js';
import { assertGasBalances } from '../utils/balances.js';
import { assertSigner } from '../utils/keys.js';
@ -61,3 +68,15 @@ export async function runPreflightChecksForChains({
await assertGasBalances(multiProvider, signer, chains, minGas);
logGreen('Balances are sufficient ✅');
}
// from parsed types
export function isISMConfig(
config: ChainMap<MultisigConfig> | ChainMap<IsmConfig>,
): boolean {
return Object.values(config).some((c) => 'type' in c);
}
// directly from filepath
export function isZODISMConfig(filepath: string): boolean {
return parseIsmConfig(filepath).success;
}

@ -0,0 +1,85 @@
import { expect } from 'chai';
import { ChainMap, IsmConfig, IsmType } from '@hyperlane-xyz/sdk';
import { readIsmConfig } from '../config/ism.js';
describe('readIsmConfig', () => {
it('parses and validates example correctly', () => {
const ism = readIsmConfig('examples/ism-advanced.yaml');
const exampleIsmConfig: ChainMap<IsmConfig> = {
anvil1: {
type: IsmType.ROUTING,
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720',
domains: {
anvil2: {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.MESSAGE_ID_MULTISIG,
threshold: 1,
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'],
},
{
type: IsmType.MERKLE_ROOT_MULTISIG,
threshold: 1,
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'],
},
],
threshold: 1,
},
},
},
anvil2: {
type: IsmType.ROUTING,
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720',
domains: {
anvil1: {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.MESSAGE_ID_MULTISIG,
threshold: 1,
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'],
},
{
type: IsmType.MERKLE_ROOT_MULTISIG,
threshold: 1,
validators: ['0xa0ee7a142d267c1f36714e4a8f75612f20a79720'],
},
],
threshold: 1,
},
},
},
};
expect(ism).to.deep.equal(exampleIsmConfig);
});
it('parsing failure, missing internal key "threshold"', () => {
expect(function () {
readIsmConfig('src/tests/ism/safe-parse-fail.yaml');
}).to.throw();
});
it('parsing failure, routingIsm.domains includes destination domain', () => {
expect(function () {
readIsmConfig('src/tests/ism/routing-same-chain-fail.yaml');
}).to.throw(
'Cannot set RoutingIsm.domain to the same chain you are configuring',
);
});
it('parsing failure, wrong ism type', () => {
expect(function () {
readIsmConfig('src/tests/ism/wrong-ism-type-fail.yaml');
}).to.throw('Invalid ISM config: anvil2 => Invalid input');
});
it('parsing failure, threshold > modules.length', () => {
expect(function () {
readIsmConfig('src/tests/ism/threshold-gt-modules-length-fail.yaml');
}).to.throw('Threshold cannot be greater than number of modules');
});
});

@ -0,0 +1,33 @@
anvil1:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil1:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1
anvil2:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil2:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1

@ -0,0 +1,32 @@
anvil1:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil2:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1
anvil2:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil1:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'

@ -0,0 +1,33 @@
anvil1:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil2:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1
anvil2:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil1:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 3

@ -0,0 +1,33 @@
anvil1:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil2:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: merkleRootMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1
anvil2:
type: domainRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains:
anvil1:
type: staticAggregationIsm
modules:
- type: messageIdMultisigIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
- type: domainRoutingIsm
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
threshold: 1

@ -0,0 +1,41 @@
import { expect } from 'chai';
import { ChainMap, MultisigConfig } from '@hyperlane-xyz/sdk';
import { readMultisigConfig } from '../config/multisig.js';
describe('readMultisigConfig', () => {
it('parses and validates example correctly', () => {
const multisig = readMultisigConfig('examples/ism.yaml');
const exampleMultisigConfig: ChainMap<MultisigConfig> = {
anvil1: {
threshold: 1,
validators: ['0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'],
},
anvil2: {
threshold: 1,
validators: ['0xa0Ee7A142d267C1f36714E4a8F75612F20a79720'],
},
};
expect(multisig).to.deep.equal(exampleMultisigConfig);
});
it('parsing failure', () => {
expect(function () {
readMultisigConfig('src/tests/multisig/safe-parse-fail.yaml');
}).to.throw('Invalid multisig config: anvil2,validators => Required');
});
it('threshold cannot be greater than the # of validators', () => {
expect(function () {
readMultisigConfig('src/tests/multisig/threshold-gt-fail.yaml');
}).to.throw('Threshold cannot be greater than number of validators');
});
it('invalid address', () => {
expect(function () {
readMultisigConfig('src/tests/multisig/invalid-address-fail.yaml');
}).to.throw('Invalid address 0xa0ee7a142d267c1n36714e4a8f7561f20a79720');
});
});

@ -0,0 +1,8 @@
anvil1:
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
anvil2:
threshold: 1
validators:
- '0xa0ee7a142d267c1n36714e4a8f7561f20a79720'

@ -0,0 +1,6 @@
anvil1:
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
anvil2:
threshold: 1

@ -0,0 +1,8 @@
anvil1:
threshold: 1 # Number: Signatures required to approve a message
validators: # Array: List of validator addresses
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
anvil2:
threshold: 3
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'

@ -5,6 +5,7 @@ import chalk from 'chalk';
import {
ChainMap,
ChainMetadata,
ChainName,
mainnetChainsMetadata,
testnetChainsMetadata,
} from '@hyperlane-xyz/sdk';
@ -32,8 +33,9 @@ export async function runSingleChainSelectionStep(
export async function runMultiChainSelectionStep(
customChains: ChainMap<ChainMetadata>,
message = 'Select chains',
chainsToFilterOut: ChainName[] = [],
) {
const choices = getChainChoices(customChains);
const choices = getChainChoices(customChains, chainsToFilterOut);
while (true) {
logTip('Use SPACE key to select chains, then press ENTER');
const chains = (await checkbox({
@ -47,9 +49,14 @@ export async function runMultiChainSelectionStep(
}
}
function getChainChoices(customChains: ChainMap<ChainMetadata>) {
function getChainChoices(
customChains: ChainMap<ChainMetadata>,
chainsToFilterOut: ChainName[] = [],
) {
const chainsToChoices = (chains: ChainMetadata[]) =>
chains.map((c) => ({ name: c.name, value: c.name }));
chains
.filter((chain) => !chainsToFilterOut.includes(chain.name))
.map((c) => ({ name: c.name, value: c.name }));
const choices: Parameters<typeof select>['0']['choices'] = [
new Separator('--Custom Chains--'),
...chainsToChoices(Object.values(customChains)),

@ -127,7 +127,10 @@ export {
collectValidators,
moduleCanCertainlyVerify,
} from './ism/HyperlaneIsmFactory';
export { buildMultisigIsmConfigs } from './ism/multisig';
export {
buildAggregationIsmConfigs,
buildMultisigIsmConfigs,
} from './ism/multisig';
export {
AggregationIsmConfig,
DeployedIsm,

@ -2,7 +2,12 @@ import { objFilter, objMap } from '@hyperlane-xyz/utils';
import { ChainMap, ChainName } from '../types';
import { MultisigConfig, MultisigIsmConfig } from './types';
import {
AggregationIsmConfig,
IsmType,
MultisigConfig,
MultisigIsmConfig,
} from './types';
// build multisigIsmConfig from multisigConfig
// eg. for { sepolia (local), arbitrumsepolia, scrollsepolia }
@ -25,3 +30,31 @@ export const buildMultisigIsmConfigs = (
}),
);
};
export const buildAggregationIsmConfigs = (
local: ChainName,
chains: ChainName[],
multisigConfigs: ChainMap<MultisigConfig>,
): ChainMap<AggregationIsmConfig> => {
return objMap(
objFilter(
multisigConfigs,
(chain, config): config is MultisigConfig =>
chain !== local && chains.includes(chain),
),
(_, config) => ({
type: IsmType.AGGREGATION,
modules: [
{
...config,
type: IsmType.MESSAGE_ID_MULTISIG,
},
{
...config,
type: IsmType.MERKLE_ROOT_MULTISIG,
},
],
threshold: 1,
}),
) as ChainMap<AggregationIsmConfig>;
};

@ -16953,7 +16953,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:5.1.6, typescript@npm:^5.1.6":
"typescript@npm:5.1.6":
version: 5.1.6
resolution: "typescript@npm:5.1.6"
bin:
@ -16963,7 +16963,17 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A5.1.6#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.1.6#optional!builtin<compat/typescript>":
"typescript@npm:^5.1.6":
version: 5.3.2
resolution: "typescript@npm:5.3.2"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 415e5fb6611f5713e460bad48039f00bcfdbde53a2f911727862d5aa9c5d5edd250059a419df382d8f031709e15a169c41eb62b6a401da5eec7ac0f4e359d6ac
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A5.1.6#optional!builtin<compat/typescript>":
version: 5.1.6
resolution: "typescript@patch:typescript@npm%3A5.1.6#optional!builtin<compat/typescript>::version=5.1.6&hash=5da071"
bin:
@ -16973,6 +16983,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.1.6#optional!builtin<compat/typescript>":
version: 5.3.2
resolution: "typescript@patch:typescript@npm%3A5.3.2#optional!builtin<compat/typescript>::version=5.3.2&hash=e012d7"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 1b45cdfb577a78ae7a9a9d0b77a7b772142cb98ba05e4e5aefba7044a028ded885bcecef63166407a5986645cea816fe4986894336aacd5e791796ea79a6a7ed
languageName: node
linkType: hard
"typical@npm:^4.0.0":
version: 4.0.0
resolution: "typical@npm:4.0.0"

Loading…
Cancel
Save