Skip to content

Commit

Permalink
Cherrypick key refactor onto v2 (#3034)
Browse files Browse the repository at this point in the history
### Description

- cherry-picks #3023, which is necessary to stop funding the GCP relayer
key
- makes some additional small changes required to work well with key
funder, specifically to filter out non-EVM chains and non-blessed chains
(like Nautilus). The latter is because the environment specific
multiprovider doesn't include non-blessed chains like Nautilus, and
adding it messes up a bunch of our other core tooling
- small change to #3023 to read in helloworld contract addresses from
the correct environment that i will bring to `main` too

### Drive-by changes

- fix deploy chains for key funder

### Related issues

Part of #3024 

### Backward compatibility

ye

### Testing

ran funding script locally

---------

Co-authored-by: OttBunn <[email protected]>
  • Loading branch information
tkporter and ottbunn authored Dec 7, 2023
1 parent a9665b9 commit 4591df7
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 218 deletions.
2 changes: 1 addition & 1 deletion typescript/infra/config/environments/mainnet2/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: '8b752d0-20230606-195641',
tag: '98658d0-20231207-121541',
},
// We're currently using the same deployer key as mainnet.
// To minimize nonce clobbering we offset the key funder cron
Expand Down
2 changes: 1 addition & 1 deletion typescript/infra/config/environments/testnet3/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: '8b752d0-20230606-195641',
tag: '98658d0-20231207-121541',
},
// We're currently using the same deployer key as testnet2.
// To minimize nonce clobbering we offset the key funder cron
Expand Down
207 changes: 123 additions & 84 deletions typescript/infra/scripts/funding/fund-keys-from-deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,36 @@ import { format } from 'util';

import {
AgentConnectionType,
AllChains,
ChainMap,
ChainName,
Chains,
HyperlaneIgp,
MultiProvider,
} from '@hyperlane-xyz/sdk';
import { error, log, warn } from '@hyperlane-xyz/utils';
import {
error,
log,
objFilter,
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 { AgentRole, Role } from '../../src/roles';
import { Role } from '../../src/roles';
import { submitMetrics } from '../../src/utils/metrics';
import {
assertContext,
assertRole,
isEthereumProtocolChain,
readJSONAtPath,
} from '../../src/utils/utils';
import { getAgentConfig, getArgs, getEnvironmentConfig } from '../utils';
Expand Down Expand Up @@ -217,9 +223,9 @@ async function main() {
ContextFunder.fromSerializedAddressFile(
environment,
multiProvider,
path,
argv.contextsAndRoles,
argv.skipIgpClaim,
path,
),
);
} else {
Expand Down Expand Up @@ -254,78 +260,115 @@ 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,
) {
// At the moment, only blessed EVM chains are supported
roleKeysPerChain = objFilter(
roleKeysPerChain,
(chain, _roleKeys): _roleKeys is Record<Role, BaseCloudAgentKey[]> => {
const valid =
isEthereumProtocolChain(chain) &&
multiProvider.tryGetChainName(chain) !== null;
if (!valid) {
warn('Skipping funding for non-blessed or non-Ethereum chain', {
chain,
});
}
return valid;
},
);
this.igp = HyperlaneIgp.fromEnvironment(
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 @@ -341,12 +384,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 @@ -356,10 +409,9 @@ class ContextFunder {
// Funds all the roles in this.rolesToFund
// Returns whether a failure occurred.
async fund(): Promise<boolean> {
let failureOccurred = false;

const chainKeys = this.getChainKeys();
const promises = Object.entries(chainKeys).map(async ([chain, keys]) => {
const chainKeyEntries = Object.entries(this.keysToFundPerChain);
const promises = chainKeyEntries.map(async ([chain, keys]) => {
let failureOccurred = false;
if (keys.length > 0) {
if (!this.skipIgpClaim) {
failureOccurred ||= await gracefullyHandleError(
Expand All @@ -379,38 +431,29 @@ class ContextFunder {
const failure = await this.attemptToFundKey(key, chain);
failureOccurred ||= failure;
}
return failureOccurred;
});

try {
await Promise.all(promises);
} catch (e) {
error('Unhandled error when funding key', { error: format(e) });
failureOccurred = true;
}
// A failure occurred if any of the promises rejected or
// if any of them resolved with true, indicating a failure
// somewhere along the way
const failureOccurred = (await Promise.allSettled(promises)).reduce(
(failureAgg, result, i) => {
if (result.status === 'rejected') {
error('Funding promise for chain rejected', {
chain: chainKeyEntries[i][0],
error: format(result.reason),
});
return true;
}
return result.value || failureAgg;
},
false,
);

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;
for (const chain of chains[role as AgentRole]) {
chainKeys[chain].push(key);
}
}
}
return chainKeys;
}

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

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

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

import { AgentGCPKey } from './gcp';

Expand Down Expand Up @@ -127,7 +127,8 @@ export abstract class AgentHelmManager {
}

connectionType(chain: ChainName): AgentConnectionType {
if (chainMetadata[chain].protocol == ProtocolType.Sealevel) {
// Non-Ethereum chains only support Http
if (!isEthereumProtocolChain(chain)) {
return AgentConnectionType.Http;
}

Expand Down
Loading

0 comments on commit 4591df7

Please sign in to comment.