feat(sdk): Sort ISM config arrays (#4436)

### Description

- Introduce sorting validator arrays and modules by type for ISM in the
`normalizeConfig` method
- This will avoid deploying a new version of the same ISM (in practice)
for updates
- Sort staticAddressSets by value before deploying
- This will enable us to use ISM recovery
- This will also avoid raising ISM mismatch violations for our checker
tooling

### Testing

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->

Unit Tests
pull/4477/head
Mohammed Hussan 2 months ago committed by GitHub
parent 085e7d0463
commit d6de34ad55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/chilled-coats-boil.md
  2. 5
      .changeset/tender-fishes-sniff.md
  3. 3
      typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts
  4. 22
      typescript/sdk/src/deploy/EvmModuleDeployer.ts
  5. 9
      typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts
  6. 2
      typescript/sdk/src/hook/EvmHookModule.ts
  7. 214
      typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts
  8. 2
      typescript/sdk/src/ism/EvmIsmModule.ts
  9. 2
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  10. 2
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  11. 60
      typescript/sdk/src/utils/ism.ts
  12. 1
      typescript/utils/src/index.ts
  13. 20
      typescript/utils/src/objects.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/utils': minor
---
Add sortArraysInConfig method, normalizeConfig implementation to call sortArraysInConfig after current behavior

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Sort values in EvmModuleDeployer.deployStaticAddressSet

@ -10,13 +10,14 @@ import {
TimelockController__factory,
ValidatorAnnounce__factory,
} from '@hyperlane-xyz/core';
import { normalizeConfig, objMap } from '@hyperlane-xyz/utils';
import { objMap } from '@hyperlane-xyz/utils';
import { TestChainName } from '../consts/testChains.js';
import { IsmConfig, IsmType } from '../ism/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { randomAddress, testCoreConfig } from '../test/testUtils.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmCoreModule } from './EvmCoreModule.js';
import { CoreConfig } from './types.js';

@ -264,34 +264,40 @@ export class EvmModuleDeployer<Factories extends HyperlaneFactories> {
threshold?: number;
multiProvider: MultiProvider;
}): Promise<Address> {
const sortedValues = [...values].sort();
const address = await factory['getAddress(address[],uint8)'](
values,
sortedValues,
threshold,
);
const code = await multiProvider.getProvider(chain).getCode(address);
if (code === '0x') {
logger.debug(
`Deploying new ${threshold} of ${values.length} address set to ${chain}`,
`Deploying new ${threshold} of ${sortedValues.length} address set to ${chain}`,
);
const overrides = multiProvider.getTransactionOverrides(chain);
// estimate gas
const estimatedGas = await factory.estimateGas['deploy(address[],uint8)'](
values,
sortedValues,
threshold,
overrides,
);
// add 10% buffer
const hash = await factory['deploy(address[],uint8)'](values, threshold, {
...overrides,
gasLimit: estimatedGas.add(estimatedGas.div(10)), // 10% buffer
});
const hash = await factory['deploy(address[],uint8)'](
sortedValues,
threshold,
{
...overrides,
gasLimit: estimatedGas.add(estimatedGas.div(10)), // 10% buffer
},
);
await multiProvider.handleTx(chain, hash);
} else {
logger.debug(
`Recovered ${threshold} of ${values.length} address set on ${chain}: ${address}`,
`Recovered ${threshold} of ${sortedValues.length} address set on ${chain}: ${address}`,
);
}

@ -3,13 +3,7 @@ import { expect } from 'chai';
import { Signer } from 'ethers';
import hre from 'hardhat';
import {
Address,
assert,
deepEquals,
eqAddress,
normalizeConfig,
} from '@hyperlane-xyz/utils';
import { Address, assert, deepEquals, eqAddress } from '@hyperlane-xyz/utils';
import { TestChainName, testChains } from '../consts/testChains.js';
import { HyperlaneAddresses, HyperlaneContracts } from '../contracts/types.js';
@ -20,6 +14,7 @@ import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomAddress, randomInt } from '../test/testUtils.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmHookModule } from './EvmHookModule.js';
import {

@ -30,7 +30,6 @@ import {
addressToBytes32,
deepEquals,
eqAddress,
normalizeConfig,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -51,6 +50,7 @@ import { ArbL2ToL1IsmConfig, IsmType, OpStackIsmConfig } from '../ism/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmHookReader } from './EvmHookReader.js';
import { DeployedHook, HookFactories, hookFactories } from './contracts.js';

@ -4,7 +4,7 @@ import { expect } from 'chai';
import { Signer } from 'ethers';
import hre from 'hardhat';
import { Address, eqAddress, normalizeConfig } from '@hyperlane-xyz/utils';
import { Address, eqAddress } from '@hyperlane-xyz/utils';
import { TestChainName, testChains } from '../consts/testChains.js';
import { HyperlaneAddresses, HyperlaneContracts } from '../contracts/types.js';
@ -13,6 +13,7 @@ import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDe
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomAddress, randomInt } from '../test/testUtils.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmIsmModule } from './EvmIsmModule.js';
import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js';
@ -36,19 +37,27 @@ const randomMultisigIsmConfig = (m: number, n: number): MultisigIsmConfig => {
};
};
const ModuleTypes = [
ModuleType.AGGREGATION,
ModuleType.MERKLE_ROOT_MULTISIG,
ModuleType.ROUTING,
ModuleType.NULL,
];
const NonNestedModuleTypes = [ModuleType.MERKLE_ROOT_MULTISIG, ModuleType.NULL];
function randomModuleType(): ModuleType {
const choices = [
ModuleType.AGGREGATION,
ModuleType.MERKLE_ROOT_MULTISIG,
ModuleType.ROUTING,
ModuleType.NULL,
];
return choices[randomInt(choices.length)];
return ModuleTypes[randomInt(ModuleTypes.length)];
}
function randomNonNestedModuleType(): ModuleType {
return NonNestedModuleTypes[randomInt(NonNestedModuleTypes.length)];
}
const randomIsmConfig = (depth = 0, maxDepth = 2): IsmConfig => {
const randomIsmConfig = (depth = 0, maxDepth = 2) => {
const moduleType =
depth == maxDepth ? ModuleType.MERKLE_ROOT_MULTISIG : randomModuleType();
depth === maxDepth ? randomNonNestedModuleType() : randomModuleType();
switch (moduleType) {
case ModuleType.MERKLE_ROOT_MULTISIG: {
const n = randomInt(5, 1);
@ -65,10 +74,21 @@ const randomIsmConfig = (depth = 0, maxDepth = 2): IsmConfig => {
return config;
}
case ModuleType.AGGREGATION: {
const n = randomInt(5, 1);
const modules = new Array<number>(n)
.fill(0)
.map(() => randomIsmConfig(depth + 1));
const n = randomInt(2, 1);
const moduleTypes = new Set();
const modules = new Array<number>(n).fill(0).map(() => {
let moduleConfig;
let moduleType;
// Ensure that we do not add the same module type more than once per level
do {
moduleConfig = randomIsmConfig(depth + 1, maxDepth);
moduleType = moduleConfig.type;
} while (moduleTypes.has(moduleType));
moduleTypes.add(moduleType);
return moduleConfig;
});
const config: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
threshold: randomInt(n, 1),
@ -176,8 +196,10 @@ describe('EvmIsmModule', async () => {
// expect that the ISM matches the config after all tests
afterEach(async () => {
const normalizedDerivedConfig = normalizeConfig(await testIsm.read());
const derivedConfiig = await testIsm.read();
const normalizedDerivedConfig = normalizeConfig(derivedConfiig);
const normalizedConfig = normalizeConfig(testConfig);
assert.deepStrictEqual(normalizedDerivedConfig, normalizedConfig);
});
@ -347,6 +369,74 @@ describe('EvmIsmModule', async () => {
.true;
});
it(`reordering validators in an existing ${type} should not trigger a redeployment`, async () => {
// create a new ISM
const routerConfig = {
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
],
threshold: 2,
},
},
};
const { ism, initialIsmAddress } = await createIsm(
routerConfig as RoutingIsmConfig,
);
const updatedRouterConfig = {
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
],
threshold: 2,
},
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
],
threshold: 2,
},
},
};
// expect 0 updates
await expectTxsAndUpdate(
ism,
updatedRouterConfig as RoutingIsmConfig,
0,
);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
it(`update owner in an existing ${type} not owned by deployer`, async () => {
// ISM owner is not the deployer
exampleRoutingConfig.owner = randomAddress();
@ -435,5 +525,99 @@ describe('EvmIsmModule', async () => {
.true;
});
}
it(`reordering modules in an existing staticAggregationIsm should not trigger a redeployment`, async () => {
// create a new ISM
const config: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
{
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
],
threshold: 2,
},
},
},
],
threshold: 2,
};
const { ism, initialIsmAddress } = await createIsm(
config as AggregationIsmConfig,
);
const updatedConfig: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
modules: [
{
type: IsmType.ROUTING,
owner: (await multiProvider.getSignerAddress(chain)).toLowerCase(),
domains: {
test2: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
],
threshold: 2,
},
test1: {
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
},
},
{
type: IsmType.MERKLE_ROOT_MULTISIG,
validators: [
'0x5FbDB2315678afecb367f032d93F642f64180aa3',
'0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2',
'0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db',
],
threshold: 2,
},
],
threshold: 2,
};
// expect 0 updates
await expectTxsAndUpdate(ism, updatedConfig, 0);
// expect the ISM address to be the same
expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be
.true;
});
});
});

@ -26,7 +26,6 @@ import {
assert,
deepEquals,
eqAddress,
normalizeConfig,
objFilter,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -46,6 +45,7 @@ import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainName, ChainNameOrId } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';
import { findMatchingLogEvents } from '../utils/logUtils.js';
import { EvmIsmReader } from './EvmIsmReader.js';

@ -24,7 +24,6 @@ import {
TestChainName,
serializeContracts,
} from '@hyperlane-xyz/sdk';
import { normalizeConfig } from '@hyperlane-xyz/utils';
import { TestCoreApp } from '../core/TestCoreApp.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
@ -36,6 +35,7 @@ import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { RemoteRouters } from '../router/types.js';
import { randomAddress } from '../test/testUtils.js';
import { ChainMap } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmERC20WarpModule } from './EvmERC20WarpModule.js';
import { TokenType } from './config.js';

@ -12,7 +12,6 @@ import {
assert,
deepEquals,
isObjEmpty,
normalizeConfig,
rootLogger,
} from '@hyperlane-xyz/utils';
@ -25,6 +24,7 @@ import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { HypERC20Deployer } from './deploy.js';

@ -1,3 +1,5 @@
import { WithAddress } from '@hyperlane-xyz/utils';
import { multisigIsmVerifyCosts } from '../consts/multisigIsmVerifyCosts.js';
export function multisigIsmVerificationCost(m: number, n: number): number {
@ -11,3 +13,61 @@ export function multisigIsmVerificationCost(m: number, n: number): number {
// @ts-ignore
return multisigIsmVerifyCosts[`${n}`][`${m}`];
}
// Function to recursively remove 'address' properties and lowercase string properties
export function normalizeConfig(obj: WithAddress<any>): any {
return sortArraysInConfig(lowerCaseConfig(obj));
}
function lowerCaseConfig(obj: any): any {
if (Array.isArray(obj)) {
return obj.map(normalizeConfig);
} else if (obj !== null && typeof obj === 'object') {
const newObj: any = {};
for (const key in obj) {
if (key !== 'address') {
newObj[key] = key === 'type' ? obj[key] : normalizeConfig(obj[key]);
}
}
return newObj;
} else if (typeof obj === 'string') {
return obj.toLowerCase();
}
return obj;
}
// write a function that will go through an object and sort any arrays it finds
export function sortArraysInConfig(config: any): any {
// Check if the current object is an array
if (Array.isArray(config)) {
return config.map(sortArraysInConfig);
}
// Check if it's an object and not null
else if (typeof config === 'object' && config !== null) {
const sortedConfig: any = {};
for (const key in config) {
if (key === 'validators' && Array.isArray(config[key])) {
// Sort the validators array in lexicographical order (since they're already lowercase)
sortedConfig[key] = [...config[key]].sort();
}
// if key is modules or hooks, sort the objects in the array by their 'type' property
else if (
(key === 'modules' || key === 'hooks') &&
Array.isArray(config[key])
) {
sortedConfig[key] = [...config[key]].sort((a: any, b: any) => {
if (a.type < b.type) return -1;
if (a.type > b.type) return 1;
return 0;
});
} else {
// Recursively apply sorting to other fields
sortedConfig[key] = sortArraysInConfig(config[key]);
}
}
return sortedConfig;
}
return config;
}

@ -102,7 +102,6 @@ export {
invertKeysAndValues,
isObjEmpty,
isObject,
normalizeConfig,
objFilter,
objKeys,
objLength,

@ -2,7 +2,6 @@ import { cloneDeep, isEqual } from 'lodash-es';
import { stringify as yamlStringify } from 'yaml';
import { ethersBigNumberSerializer } from './logging.js';
import { WithAddress } from './types.js';
import { assert } from './validation.js';
export function isObject(item: any) {
@ -156,22 +155,3 @@ export function stringifyObject(
}
return yamlStringify(JSON.parse(json), null, space);
}
// Function to recursively remove 'address' properties and lowercase string properties
export function normalizeConfig(obj: WithAddress<any>): any {
if (Array.isArray(obj)) {
return obj.map(normalizeConfig);
} else if (obj !== null && typeof obj === 'object') {
const newObj: any = {};
for (const key in obj) {
if (key !== 'address') {
newObj[key] = key === 'type' ? obj[key] : normalizeConfig(obj[key]);
}
}
return newObj;
} else if (typeof obj === 'string') {
return obj.toLowerCase();
}
return obj;
}

Loading…
Cancel
Save