feat: improve Router enrollment with Checker/Governor (#4441)

feat: improve Router enrollment with Checker/Governor
- aggregate violations into a single RouterViolation with the diff in
router enrollments
- reduces number of txs to do the enrollment
- now supports ICA router enrollment of chains that we haven't enrolled
- previously we would only iterate over the set of routers already
onchain, and no attention was paid to new domains

example call for `ica`:

![image](https://github.com/user-attachments/assets/5557d2f4-41e5-45ee-b318-e4cf8d510069)

example call for `haas`:

![image](https://github.com/user-attachments/assets/fec0e41b-e0ef-43a0-a17c-3f77c0756f8a)
pull/4470/head
Paul Balaji 2 months ago committed by GitHub
parent a75e84af4d
commit a19e882fd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/chatty-plums-invite.md
  2. 11
      typescript/infra/src/govern/HyperlaneAppGovernor.ts
  3. 23
      typescript/infra/src/govern/ProxiedRouterGovernor.ts
  4. 6
      typescript/sdk/src/middleware/account/InterchainAccount.ts
  5. 40
      typescript/sdk/src/router/HyperlaneRouterChecker.ts
  6. 4
      typescript/sdk/src/router/RouterApps.ts
  7. 10
      typescript/sdk/src/router/types.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': minor
---
Improve Router Checker/Governor tooling to support enrolling multiple routers for missing domains

@ -111,11 +111,12 @@ export abstract class HyperlaneAppGovernor<
console.log(
`> ${calls.length} calls will be submitted via ${SubmissionType[submissionType]}`,
);
calls.map((c) =>
console.log(
`> > ${c.description} (to: ${c.to} data: ${c.data}) value: ${c.value}`,
),
);
calls.map((c) => {
console.log(`> > ${c.description}`);
console.log(`> > > to: ${c.to}`);
console.log(`> > > data: ${c.data}`);
console.log(`> > > value: ${c.value}`);
});
if (!requestConfirmation) return true;
const { value: confirmed } = await prompts({

@ -12,6 +12,7 @@ import {
RouterViolationType,
ViolationType,
} from '@hyperlane-xyz/sdk';
import { stringifyObject } from '@hyperlane-xyz/utils';
import { HyperlaneAppGovernor } from './HyperlaneAppGovernor.js';
@ -54,19 +55,29 @@ export class ProxiedRouterGovernor<
}
protected handleEnrolledRouterViolation(violation: RouterViolation) {
const remoteDomain = this.checker.multiProvider.getDomainId(
violation.remoteChain,
);
const expectedDomains: number[] = [];
const expectedAddresses: string[] = [];
for (const [remoteChain, routerDiff] of Object.entries(
violation.routerDiff,
)) {
const remoteDomain = this.checker.multiProvider.getDomainId(remoteChain);
expectedDomains.push(remoteDomain);
expectedAddresses.push(routerDiff.expected);
}
return {
chain: violation.chain,
call: {
to: violation.contract.address,
data: violation.contract.interface.encodeFunctionData(
'enrollRemoteRouter',
[remoteDomain, violation.expected],
'enrollRemoteRouters',
[expectedDomains, expectedAddresses],
),
value: BigNumber.from(0),
description: `Enroll router for remote chain ${violation.remoteChain} (${remoteDomain}) ${violation.expected} in ${violation.contract.address}`,
description: `Updating routers in ${violation.contract.address} for ${
expectedDomains.length
} remote chains:\n${stringifyObject(violation.routerDiff, 'yaml')}`,
},
};
}

@ -31,6 +31,12 @@ export class InterchainAccount extends RouterApp<InterchainAccountFactories> {
super(contractsMap, multiProvider);
}
override async remoteChains(chainName: string): Promise<ChainName[]> {
return Object.keys(this.contractsMap).filter(
(chain) => chain !== chainName,
);
}
router(
contracts: HyperlaneContracts<InterchainAccountFactories>,
): InterchainAccountRouter {

@ -1,6 +1,12 @@
import { ethers } from 'ethers';
import { addressToBytes32, assert, eqAddress } from '@hyperlane-xyz/utils';
import {
AddressBytes32,
addressToBytes32,
assert,
eqAddress,
rootLogger,
} from '@hyperlane-xyz/utils';
import { HyperlaneFactories } from '../contracts/types.js';
import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker.js';
@ -29,6 +35,7 @@ export class HyperlaneRouterChecker<
app: App,
configMap: ChainMap<Config>,
readonly ismFactory?: HyperlaneIsmFactory,
readonly logger = rootLogger.child({ module: 'HyperlaneRouterChecker' }),
) {
super(multiProvider, app, configMap);
}
@ -120,24 +127,43 @@ export class HyperlaneRouterChecker<
async checkEnrolledRouters(chain: ChainName): Promise<void> {
const router = this.app.router(this.app.getContracts(chain));
const remoteChains = await this.app.remoteChains(chain);
const currentRouters: ChainMap<string> = {};
const expectedRouters: ChainMap<string> = {};
const routerDiff: ChainMap<{
actual: AddressBytes32;
expected: AddressBytes32;
}> = {};
await Promise.all(
remoteChains.map(async (remoteChain) => {
const remoteRouterAddress = this.app.routerAddress(remoteChain);
const remoteDomainId = this.multiProvider.getDomainId(remoteChain);
const actualRouter = await router.routers(remoteDomainId);
const expectedRouter = addressToBytes32(remoteRouterAddress);
currentRouters[remoteChain] = actualRouter;
expectedRouters[remoteChain] = expectedRouter;
if (actualRouter !== expectedRouter) {
const violation: RouterViolation = {
chain,
remoteChain,
type: RouterViolationType.EnrolledRouter,
contract: router,
routerDiff[remoteChain] = {
actual: actualRouter,
expected: expectedRouter,
};
this.addViolation(violation);
}
}),
);
if (Object.keys(routerDiff).length > 0) {
const violation: RouterViolation = {
chain,
type: RouterViolationType.EnrolledRouter,
contract: router,
actual: currentRouters,
expected: expectedRouters,
routerDiff,
description: `Routers for some domains are missing or not enrolled correctly`,
};
this.addViolation(violation);
}
}
}

@ -45,14 +45,14 @@ export abstract class RouterApp<
// check onchain for remote enrollments
override async remoteChains(chainName: string): Promise<ChainName[]> {
const router = this.router(this.contractsMap[chainName]);
const domainIds = (await router.domains()).map((domain) => {
const onchainRemoteChainNames = (await router.domains()).map((domain) => {
const chainName = this.multiProvider.tryGetChainName(domain);
if (chainName === null) {
throw new Error(`Chain name not found for domain: ${domain}`);
}
return chainName;
});
return domainIds;
return onchainRemoteChainNames;
}
getSecurityModules(): Promise<ChainMap<Address>> {

@ -6,11 +6,12 @@ import {
Router,
TimelockController__factory,
} from '@hyperlane-xyz/core';
import { Address } from '@hyperlane-xyz/utils';
import { Address, AddressBytes32 } from '@hyperlane-xyz/utils';
import { HyperlaneFactories } from '../contracts/types.js';
import { UpgradeConfig } from '../deploy/proxy.js';
import { CheckerViolation } from '../deploy/types.js';
import { ChainMap } from '../types.js';
import {
GasRouterConfigSchema,
@ -56,10 +57,11 @@ export enum RouterViolationType {
export interface RouterViolation extends CheckerViolation {
type: RouterViolationType.EnrolledRouter;
remoteChain: string;
contract: Router;
actual: string;
expected: string;
routerDiff: ChainMap<{
actual: AddressBytes32;
expected: AddressBytes32;
}>;
description?: string;
}

Loading…
Cancel
Save