Skip to content

Commit

Permalink
Refactor key management in infra and key funder (#3023)
Browse files Browse the repository at this point in the history
### Description

* Remove the `bank` role, which we haven't used since the inception of
abacus / hyperlane
* Big changes to `key-utils.ts` so that there's a single source of truth
on what kind of keys are used depending on the role & chain. Before this
was sprinkled in a few different places
* You can now get an object of `{ [chain]: { [role]: keys[] } }`, so
it's super clear what kind of key relates to which chain. For example,
before we would use the AWS-based relayer key for EVM chains, and then a
GCP-based relayer key for non-EVM chains. But this wasn't really honored
by key funder - it had no way of knowing to only fund the AWS relayer on
EVM chains, and only fund the GCP relayer on non-EVM chains. Same
situation for Kathy, where we want to use AWS keys for EVM chains but
the GCP key for non-EVM chains
* On v2 and prior to that we were using the AWS-based key for Kathy.
Originally, we also launched v3 this way. However it was changed on v3
to use the GCP key for Kathy, causing us to fund both types of addresses
on v3. This makes it more clear that we should be using the AWS-based
key for Kathy on EVM chains

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

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

None/Manual/Unit Tests
-->
  • Loading branch information
tkporter authored Dec 6, 2023
1 parent e06fe0b commit 1a64cef
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 240 deletions.
2 changes: 1 addition & 1 deletion solidity/test/isms/DomainRoutingIsm.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ contract DomainRoutingIsmTest is Test {
_domains[i] = domain - i;
_isms[i] = deployTestIsm(bytes32(0));
}
ism = factory.deploy(_domains, _isms);
ism = factory.deploy(address(this), _domains, _isms);
for (uint256 i = 0; i < count; ++i) {
assertEq(address(ism.module(_domains[i])), address(_isms[i]));
}
Expand Down
2 changes: 1 addition & 1 deletion typescript/infra/config/environments/mainnet3/funding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { environment } from './chains';
export const keyFunderConfig: KeyFunderConfig = {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: '4a3e6ee-20231025-192258',
tag: 'e152268-20231205-122327',
},
// We're currently using the same deployer key as mainnet.
// To minimize nonce clobbering we offset the key funder cron
Expand Down
7 changes: 4 additions & 3 deletions typescript/infra/config/environments/mainnet3/helloworld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const hyperlane: HelloWorldConfig = {
kathy: {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'e21e020-20231201-111649',
tag: 'e152268-20231205-122327',
},
chainsToSkip: [],
runEnv: environment,
Expand All @@ -34,7 +34,7 @@ export const releaseCandidate: HelloWorldConfig = {
kathy: {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'e21e020-20231201-111649',
tag: 'e152268-20231205-122327',
},
chainsToSkip: [],
runEnv: environment,
Expand All @@ -50,5 +50,6 @@ export const releaseCandidate: HelloWorldConfig = {

export const helloWorld = {
[Contexts.Hyperlane]: hyperlane,
// [Contexts.ReleaseCandidate]: releaseCandidate,
[Contexts.ReleaseCandidate]: releaseCandidate,
[Contexts.Neutron]: undefined,
};
2 changes: 1 addition & 1 deletion typescript/infra/config/environments/testnet4/funding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { environment } from './chains';
export const keyFunderConfig: KeyFunderConfig = {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'cfaf553-20231009-174629',
tag: 'e152268-20231205-122327',
},
// We're currently using the same deployer key as testnet2.
// To minimize nonce clobbering we offset the key funder cron
Expand Down
4 changes: 2 additions & 2 deletions typescript/infra/config/environments/testnet4/helloworld.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const hyperlaneHelloworld: HelloWorldConfig = {
kathy: {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'e21e020-20231201-111649',
tag: 'e152268-20231205-122327',
},
chainsToSkip: [],
runEnv: environment,
Expand All @@ -33,7 +33,7 @@ export const releaseCandidateHelloworld: HelloWorldConfig = {
kathy: {
docker: {
repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo',
tag: 'e21e020-20231201-111649',
tag: 'e152268-20231205-122327',
},
chainsToSkip: [],
runEnv: environment,
Expand Down
168 changes: 88 additions & 80 deletions typescript/infra/scripts/funding/fund-keys-from-deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ import { Gauge, Registry } from 'prom-client';
import { format } from 'util';

import {
AllChains,
ChainMap,
ChainName,
Chains,
HyperlaneIgp,
MultiProvider,
RpcConsensusType,
} from '@hyperlane-xyz/sdk';
import { Address, error, log, warn } from '@hyperlane-xyz/utils';
import {
Address,
error,
log,
objMap,
promiseObjAll,
warn,
} from '@hyperlane-xyz/utils';

import { Contexts } from '../../config/contexts';
import { parseKeyIdentifier } from '../../src/agents/agent';
import { getAllCloudAgentKeys } from '../../src/agents/key-utils';
import { KeyAsAddress, getRoleKeysPerChain } from '../../src/agents/key-utils';
import {
BaseCloudAgentKey,
ReadOnlyCloudAgentKey,
} from '../../src/agents/keys';
import { DeployEnvironment } from '../../src/config';
import { deployEnvToSdkEnv } from '../../src/config/environment';
import { ContextAndRoles, ContextAndRolesMap } from '../../src/config/funding';
import { ALL_AGENT_ROLES, AgentRole, Role } from '../../src/roles';
import { Role } from '../../src/roles';
import { submitMetrics } from '../../src/utils/metrics';
import {
assertContext,
Expand Down Expand Up @@ -256,9 +262,9 @@ async function main() {
ContextFunder.fromSerializedAddressFile(
environment,
multiProvider,
path,
argv.contextsAndRoles,
argv.skipIgpClaim,
path,
),
);
} else {
Expand Down Expand Up @@ -293,10 +299,12 @@ async function main() {
class ContextFunder {
igp: HyperlaneIgp;

keysToFundPerChain: ChainMap<BaseCloudAgentKey[]>;

constructor(
public readonly environment: DeployEnvironment,
public readonly multiProvider: MultiProvider,
public readonly keys: BaseCloudAgentKey[],
roleKeysPerChain: ChainMap<Record<Role, BaseCloudAgentKey[]>>,
public readonly context: Contexts,
public readonly rolesToFund: Role[],
public readonly skipIgpClaim: boolean,
Expand All @@ -305,66 +313,86 @@ class ContextFunder {
deployEnvToSdkEnv[this.environment],
multiProvider,
);
this.keysToFundPerChain = objMap(roleKeysPerChain, (_chain, roleKeys) => {
return Object.keys(roleKeys).reduce((agg, roleStr) => {
const role = roleStr as Role;
if (this.rolesToFund.includes(role)) {
return [...agg, ...roleKeys[role]];
}
return agg;
}, [] as BaseCloudAgentKey[]);
});
}

static fromSerializedAddressFile(
environment: DeployEnvironment,
multiProvider: MultiProvider,
path: string,
contextsAndRolesToFund: ContextAndRolesMap,
skipIgpClaim: boolean,
filePath: string,
) {
log('Reading identifiers and addresses from file', {
path,
filePath,
});
const idsAndAddresses = readJSONAtPath(path);
const keys: BaseCloudAgentKey[] = idsAndAddresses
.filter((idAndAddress: any) => {
const parsed = parseKeyIdentifier(idAndAddress.identifier);
// Filter out any invalid chain names. This can happen if we're running an old
// version of this script but the list of identifiers (expected to be stored in GCP secrets)
// references newer chains.
return (
parsed.chainName === undefined ||
(AllChains as string[]).includes(parsed.chainName)
);
})
.map((idAndAddress: any) =>
ReadOnlyCloudAgentKey.fromSerializedAddress(
idAndAddress.identifier,
idAndAddress.address,
),
);

const context = keys[0].context;
// Ensure all keys have the same context, just to be safe
for (const key of keys) {
if (key.context !== context) {
throw Error(
`Expected all keys at path ${path} to have context ${context}, found ${key.context}`,
);
}
// A big array of KeyAsAddress, including keys that we may not care about.
const allIdsAndAddresses: KeyAsAddress[] = readJSONAtPath(filePath);
if (!allIdsAndAddresses.length) {
throw Error(`Expected at least one key in file ${filePath}`);
}

const rolesToFund = contextsAndRolesToFund[context];
if (!rolesToFund) {
throw Error(
`Expected context ${context} to be defined in contextsAndRolesToFund`,
);
}
// Arbitrarily pick the first key to get the context
const firstKey = allIdsAndAddresses[0];
const context = ReadOnlyCloudAgentKey.fromSerializedAddress(
firstKey.identifier,
firstKey.address,
).context;

// Indexed by the identifier for quicker lookup
const idsAndAddresses: Record<string, KeyAsAddress> =
allIdsAndAddresses.reduce((agg, idAndAddress) => {
agg[idAndAddress.identifier] = idAndAddress;
return agg;
}, {} as Record<string, KeyAsAddress>);

const agentConfig = getAgentConfig(context, environment);
// Unfetched keys per chain and role, so we know which keys
// we need. We'll use this to create a corresponding object
// of ReadOnlyCloudAgentKeys using addresses found in the
// serialized address file.
const roleKeysPerChain = getRoleKeysPerChain(agentConfig);

const readOnlyKeysPerChain = objMap(
roleKeysPerChain,
(_chain, roleKeys) => {
return objMap(roleKeys, (_role, keys) => {
return keys.map((key) => {
const idAndAddress = idsAndAddresses[key.identifier];
if (!idAndAddress) {
throw Error(
`Expected key identifier ${key.identifier} to be in file ${filePath}`,
);
}
return ReadOnlyCloudAgentKey.fromSerializedAddress(
idAndAddress.identifier,
idAndAddress.address,
);
});
});
},
);

log('Read keys for context from file', {
path,
keyCount: keys.length,
log('Successfully read keys for context from file', {
filePath,
readOnlyKeysPerChain,
context,
});

return new ContextFunder(
environment,
multiProvider,
keys,
readOnlyKeysPerChain,
context,
rolesToFund,
contextsAndRolesToFund[context]!,
skipIgpClaim,
);
}
Expand All @@ -380,12 +408,22 @@ class ContextFunder {
skipIgpClaim: boolean,
) {
const agentConfig = getAgentConfig(context, environment);
const keys = getAllCloudAgentKeys(agentConfig);
await Promise.all(keys.map((key) => key.fetch()));
const roleKeysPerChain = getRoleKeysPerChain(agentConfig);
// Fetch all the keys
await promiseObjAll(
objMap(roleKeysPerChain, (_chain, roleKeys) => {
return promiseObjAll(
objMap(roleKeys, (_role, keys) => {
return Promise.all(keys.map((key) => key.fetch()));
}),
);
}),
);

return new ContextFunder(
environment,
multiProvider,
keys,
roleKeysPerChain,
context,
rolesToFund,
skipIgpClaim,
Expand All @@ -395,8 +433,7 @@ class ContextFunder {
// Funds all the roles in this.rolesToFund
// Returns whether a failure occurred.
async fund(): Promise<boolean> {
const chainKeys = this.getChainKeys();
const chainKeyEntries = Object.entries(chainKeys);
const chainKeyEntries = Object.entries(this.keysToFundPerChain);
const promises = chainKeyEntries.map(async ([chain, keys]) => {
let failureOccurred = false;
if (keys.length > 0) {
Expand Down Expand Up @@ -441,31 +478,6 @@ class ContextFunder {
return failureOccurred;
}

private getChainKeys() {
const chainKeys: ChainMap<BaseCloudAgentKey[]> = Object.fromEntries(
// init with empty arrays
AllChains.map((c) => [c, []]),
);
for (const role of this.rolesToFund) {
const keys = this.getKeysWithRole(role);
for (const key of keys) {
const chains = getAgentConfig(
key.context,
key.environment,
).contextChainNames;
// If the role is not a relayer, we need to look up the chains for Kathy, so we'll fallback to the relayer
const roleToLookup = ALL_AGENT_ROLES.includes(role as AgentRole)
? role
: Role.Relayer;
const chainsPicked = chains[roleToLookup as AgentRole];
for (const chain of chainsPicked) {
chainKeys[chain].push(key);
}
}
}
return chainKeys;
}

private async attemptToFundKey(
key: BaseCloudAgentKey,
chain: ChainName,
Expand Down Expand Up @@ -788,10 +800,6 @@ class ContextFunder {
),
);
}

private getKeysWithRole(role: Role) {
return this.keys.filter((k) => k.role === role);
}
}

async function getAddressInfo(
Expand Down
4 changes: 2 additions & 2 deletions typescript/infra/src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
buildHelmChartDependencies,
helmifyValues,
} from '../utils/helm';
import { execCmd, isNotEthereumProtocolChain } from '../utils/utils';
import { execCmd, isEthereumProtocolChain } from '../utils/utils';

import { AgentGCPKey } from './gcp';

Expand Down Expand Up @@ -133,7 +133,7 @@ export abstract class AgentHelmManager {

rpcConsensusType(chain: ChainName): RpcConsensusType {
// Non-Ethereum chains only support Single
if (isNotEthereumProtocolChain(chain)) {
if (!isEthereumProtocolChain(chain)) {
return RpcConsensusType.Single;
}

Expand Down
Loading

0 comments on commit 1a64cef

Please sign in to comment.