diff --git a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts index 7d2788371..9cf547e8f 100644 --- a/typescript/infra/scripts/funding/fund-keys-from-deployer.ts +++ b/typescript/infra/scripts/funding/fund-keys-from-deployer.ts @@ -11,8 +11,11 @@ import { import { error, log } from '@abacus-network/utils'; import { Contexts } from '../../config/contexts'; -import { AgentKey, ReadOnlyAgentKey } from '../../src/agents/agent'; -import { getAllKeys } from '../../src/agents/key-utils'; +import { getAllCloudAgentKeys } from '../../src/agents/key-utils'; +import { + BaseCloudAgentKey, + ReadOnlyCloudAgentKey, +} from '../../src/agents/keys'; import { KEY_ROLE_ENUM } from '../../src/agents/roles'; import { ContextAndRoles, ContextAndRolesMap } from '../../src/config/funding'; import { submitMetrics } from '../../src/utils/metrics'; @@ -164,7 +167,7 @@ class ContextFunder { constructor( public readonly multiProvider: MultiProvider, - public readonly keys: AgentKey[], + public readonly keys: BaseCloudAgentKey[], public readonly context: Contexts, public readonly rolesToFund: KEY_ROLE_ENUM[], ) { @@ -184,14 +187,15 @@ class ContextFunder { path, }); const idsAndAddresses = readJSONAtPath(path); - const keys: AgentKey[] = idsAndAddresses.map((idAndAddress: any) => - ReadOnlyAgentKey.fromSerializedAddress( + const keys: BaseCloudAgentKey[] = idsAndAddresses.map((idAndAddress: any) => + ReadOnlyCloudAgentKey.fromSerializedAddress( idAndAddress.identifier, idAndAddress.address, ), ); - const context = keys[0].context; + // TODO: Why do we need to cast here? + const context = keys[0].context as Contexts; // Ensure all keys have the same context, just to be safe for (const key of keys) { if (key.context !== context) { @@ -217,7 +221,7 @@ class ContextFunder { return new ContextFunder(multiProvider, keys, context, rolesToFund); } - // The keys here are not ReadOnlyAgentKeys, instead they are AgentGCPKey or AgentAWSKeys, + // The keys here are not ReadOnlyCloudAgentKeys, instead they are AgentGCPKey or AgentAWSKeys, // which require credentials to fetch. If you want to avoid requiring credentials, use // fromSerializedAddressFile instead. static async fromContext( @@ -226,12 +230,9 @@ class ContextFunder { rolesToFund: KEY_ROLE_ENUM[], ) { const agentConfig = await getAgentConfig(context); - return new ContextFunder( - multiProvider, - getAllKeys(agentConfig), - context, - rolesToFund, - ); + const keys = getAllCloudAgentKeys(agentConfig); + await Promise.all(keys.map((key) => key.fetch())); + return new ContextFunder(multiProvider, keys, context, rolesToFund); } // Funds all the roles in this.rolesToFund @@ -290,7 +291,7 @@ class ContextFunder { } private async attemptToFundKey( - key: AgentKey, + key: BaseCloudAgentKey, chain: ChainName, ): Promise { const chainConnection = this.multiProvider.getChainConnection(chain); @@ -298,9 +299,6 @@ class ContextFunder { let failureOccurred = false; - // Some types of keys must be fetched - await key.fetch(); - try { await this.fundKeyIfRequired(chainConnection, chain, key, desiredBalance); } catch (err) { @@ -321,7 +319,7 @@ class ContextFunder { private async fundKeyIfRequired( chainConnection: ChainConnection, chain: ChainName, - key: AgentKey, + key: BaseCloudAgentKey, desiredBalance: string, ) { const currentBalance = await chainConnection.provider.getBalance( @@ -410,14 +408,12 @@ class ContextFunder { } } -function getKeyInfo(key: AgentKey) { +function getKeyInfo(key: BaseCloudAgentKey) { return { context: key.context, address: key.address, - identifier: key.identifier, originChain: key.chainName, role: key.role, - index: key.index, }; } diff --git a/typescript/infra/scripts/get-key-addresses.ts b/typescript/infra/scripts/get-key-addresses.ts index 54bb8f197..365321886 100644 --- a/typescript/infra/scripts/get-key-addresses.ts +++ b/typescript/infra/scripts/get-key-addresses.ts @@ -1,11 +1,11 @@ -import { getAllKeys } from '../src/agents/key-utils'; +import { getAllCloudAgentKeys } from '../src/agents/key-utils'; import { getContextAgentConfig } from './utils'; async function main() { const agentConfig = await getContextAgentConfig(); - const keys = getAllKeys(agentConfig); + const keys = getAllCloudAgentKeys(agentConfig); const keyInfos = await Promise.all( keys.map(async (key) => { diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index 20ccc052e..12bc92153 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -14,8 +14,8 @@ import { import { Contexts } from '../config/contexts'; import { environments } from '../config/environments'; import { getCurrentKubernetesContext } from '../src/agents'; -import { AgentKey } from '../src/agents/agent'; -import { getKey } from '../src/agents/key-utils'; +import { getCloudAgentKey } from '../src/agents/key-utils'; +import { CloudAgentKey } from '../src/agents/keys'; import { KEY_ROLE_ENUM } from '../src/agents/roles'; import { CoreEnvironmentConfig, DeployEnvironment } from '../src/config'; import { fetchProvider } from '../src/config/chain'; @@ -98,10 +98,10 @@ async function getKeyForRole( chain: Chain, role: KEY_ROLE_ENUM, index?: number, -): Promise { +): Promise { const coreConfig = getCoreEnvironmentConfig(environment); const agentConfig = await getAgentConfig(context, coreConfig); - return getKey(agentConfig, role, chain, index); + return getCloudAgentKey(agentConfig, role, chain, index); } export async function getMultiProviderForRole( diff --git a/typescript/infra/src/agents/agent.ts b/typescript/infra/src/agents/agent.ts index 54f46e8f9..7372df822 100644 --- a/typescript/infra/src/agents/agent.ts +++ b/typescript/infra/src/agents/agent.ts @@ -1,150 +1,9 @@ -import { ethers } from 'ethers'; - import { ChainName } from '@abacus-network/sdk'; import { Contexts } from '../../config/contexts'; -import { assertChain, assertContext, assertRole } from '../utils/utils'; import { KEY_ROLE_ENUM } from './roles'; -export abstract class AgentKey { - constructor( - public environment: string, - public context: Contexts, - public readonly role: KEY_ROLE_ENUM, - public readonly chainName?: ChainName, - public readonly index?: number, - ) {} - - abstract get identifier(): string; - abstract get address(): string; - - abstract fetch(): Promise; - - abstract createIfNotExists(): Promise; - abstract delete(): Promise; - // Returns new address - abstract update(): Promise; - - abstract getSigner( - provider?: ethers.providers.Provider, - ): Promise; - - serializeAsAddress() { - return { - identifier: this.identifier, - address: this.address, - }; - } -} - -export class ReadOnlyAgentKey extends AgentKey { - private _identifier: string; - private _address: string; - - constructor( - public environment: string, - public context: Contexts, - public readonly role: KEY_ROLE_ENUM, - identifier: string, - address: string, - public readonly chainName?: ChainName, - public readonly index?: number, - ) { - super(environment, context, role, chainName, index); - - this._identifier = identifier; - this._address = address; - } - - /** - * Parses the identifier, deriving the environment, role, chain (if any), and index (if any) - * and constructs a ReadOnlyAgentKey. - * @param identifier The "identifier" of the key. This can come in a few different - * flavors, e.g.: - * alias/abacus-testnet2-key-kathy (<-- abacus context, not specific to any chain) - * alias/abacus-testnet2-key-optimismkovan-relayer (<-- abacus context, chain specific) - * alias/abacus-testnet2-key-alfajores-validator-0 (<-- abacus context, chain specific and has an index) - * abacus-dev-key-kathy (<-- same idea as above, but without the `alias/` prefix if it's not AWS-based) - * alias/flowcarbon-testnet2-key-optimismkovan-relayer (<-- flowcarbon context & chain specific, intended to show that there are non-abacus contexts) - * @param address The address of the key. - * @returns A ReadOnlyAgentKey for the provided identifier and address. - */ - static fromSerializedAddress( - identifier: string, - address: string, - ): ReadOnlyAgentKey { - const regex = - /(alias\/)?([a-zA-Z0-9]+)-([a-zA-Z0-9]+)-key-([a-zA-Z0-9]+)-?([a-zA-Z0-9]+)?-?([0-9]+)?/g; - const matches = regex.exec(identifier); - if (!matches) { - throw Error('Invalid identifier'); - } - const context = assertContext(matches[2]); - const environment = matches[3]; - - // If matches[5] is undefined, this key doesn't have a chainName, and matches[4] - // is the role name. - if (matches[5] === undefined) { - return new ReadOnlyAgentKey( - environment, - context, - assertRole(matches[4]), - identifier, - address, - ); - } else if (matches[6] === undefined) { - // If matches[6] is undefined, this key doesn't have an index. - return new ReadOnlyAgentKey( - environment, - context, - assertRole(matches[5]), - identifier, - address, - assertChain(matches[4]), - ); - } else { - return new ReadOnlyAgentKey( - environment, - context, - assertRole(matches[5]), - identifier, - address, - assertChain(matches[4]), - parseInt(matches[6]), - ); - } - } - - get identifier(): string { - return this._identifier; - } - - get address(): string { - return this._address; - } - - async fetch(): Promise { - // No-op - } - - async createIfNotExists(): Promise { - throw Error('Not supported'); - } - - async delete(): Promise { - throw Error('Not supported'); - } - - async update(): Promise { - throw Error('Not supported'); - } - - async getSigner(): Promise { - throw Error('Not supported'); - } -} - export function isValidatorKey(role: string) { return role === KEY_ROLE_ENUM.Validator; } diff --git a/typescript/infra/src/agents/aws/key.ts b/typescript/infra/src/agents/aws/key.ts index 54db06fcf..39d00355c 100644 --- a/typescript/infra/src/agents/aws/key.ts +++ b/typescript/infra/src/agents/aws/key.ts @@ -22,7 +22,8 @@ import { ChainName } from '@abacus-network/sdk'; import { AgentConfig, AwsKeyConfig, KeyType } from '../../config/agent'; import { getEthereumAddress, sleep } from '../../utils/utils'; -import { AgentKey, keyIdentifier } from '../agent'; +import { keyIdentifier } from '../agent'; +import { CloudAgentKey } from '../keys'; import { KEY_ROLE_ENUM } from '../roles'; interface UnfetchedKey { @@ -36,7 +37,7 @@ interface FetchedKey { type RemoteKey = UnfetchedKey | FetchedKey; -export class AgentAwsKey extends AgentKey { +export class AgentAwsKey extends CloudAgentKey { private client: KMSClient | undefined; private region: string; public remoteKey: RemoteKey = { fetched: false }; diff --git a/typescript/infra/src/agents/gcp.ts b/typescript/infra/src/agents/gcp.ts index b880ada65..23b8486fe 100644 --- a/typescript/infra/src/agents/gcp.ts +++ b/typescript/infra/src/agents/gcp.ts @@ -6,7 +6,8 @@ import { Contexts } from '../../config/contexts'; import { fetchGCPSecret, setGCPSecret } from '../utils/gcloud'; import { execCmd, include } from '../utils/utils'; -import { AgentKey, isValidatorKey, keyIdentifier } from './agent'; +import { isValidatorKey, keyIdentifier } from './agent'; +import { CloudAgentKey } from './keys'; import { KEY_ROLE_ENUM } from './roles'; // This is the type for how the keys are persisted in GCP @@ -32,7 +33,7 @@ interface FetchedKey { type RemoteKey = UnfetchedKey | FetchedKey; -export class AgentGCPKey extends AgentKey { +export class AgentGCPKey extends CloudAgentKey { constructor( environment: string, context: Contexts, diff --git a/typescript/infra/src/agents/key-utils.ts b/typescript/infra/src/agents/key-utils.ts index 8aa1ea914..fb99d056f 100644 --- a/typescript/infra/src/agents/key-utils.ts +++ b/typescript/infra/src/agents/key-utils.ts @@ -5,9 +5,9 @@ import { AgentConfig } from '../config'; import { fetchGCPSecret, setGCPSecret } from '../utils/gcloud'; import { execCmd } from '../utils/utils'; -import { AgentKey } from './agent'; import { AgentAwsKey } from './aws/key'; import { AgentGCPKey } from './gcp'; +import { CloudAgentKey } from './keys'; import { KEY_ROLE_ENUM } from './roles'; interface KeyAsAddress { @@ -15,14 +15,14 @@ interface KeyAsAddress { address: string; } -export function getKey( +export function getCloudAgentKey( agentConfig: AgentConfig, role: KEY_ROLE_ENUM, chainName?: Chain, index?: number, -): AgentKey { - // The deployer is always GCP-based +): CloudAgentKey { if (agentConfig.aws && role !== KEY_ROLE_ENUM.Deployer) { + // The deployer is always GCP-based return new AgentAwsKey(agentConfig, role, chainName, index); } else { return new AgentGCPKey( @@ -35,38 +35,48 @@ export function getKey( } } -export function getValidatorKeys( +export function getValidatorCloudAgentKeys( agentConfig: AgentConfig, -): Array { +): Array { // For each chainName, create validatorCount keys return agentConfig.contextChainNames.flatMap((chainName) => - [...Array(agentConfig.validatorSets[chainName].validators.length)].map( - (_, index) => - getKey(agentConfig, KEY_ROLE_ENUM.Validator, chainName, index), - ), + agentConfig.validatorSets[chainName].validators + .filter((validator) => !validator.readonly) + .map((validator, index) => + getCloudAgentKey( + agentConfig, + KEY_ROLE_ENUM.Validator, + chainName, + index, + ), + ), ); } -export function getRelayerKeys(agentConfig: AgentConfig): Array { +export function getRelayerCloudAgentKeys( + agentConfig: AgentConfig, +): Array { return agentConfig.contextChainNames.map((chainName) => - getKey(agentConfig, KEY_ROLE_ENUM.Relayer, chainName), + getCloudAgentKey(agentConfig, KEY_ROLE_ENUM.Relayer, chainName), ); } -export function getAllKeys(agentConfig: AgentConfig): Array { +export function getAllCloudAgentKeys( + agentConfig: AgentConfig, +): Array { return agentConfig.rolesWithKeys.flatMap((role) => { if (role === KEY_ROLE_ENUM.Validator) { - return getValidatorKeys(agentConfig); + return getValidatorCloudAgentKeys(agentConfig); } else if (role === KEY_ROLE_ENUM.Relayer) { - return getRelayerKeys(agentConfig); + return getRelayerCloudAgentKeys(agentConfig); } else { - return [getKey(agentConfig, role)]; + return [getCloudAgentKey(agentConfig, role)]; } }); } export async function deleteAgentKeys(agentConfig: AgentConfig) { - const keys = getAllKeys(agentConfig); + const keys = getAllCloudAgentKeys(agentConfig); await Promise.all(keys.map((key) => key.delete())); await execCmd( `gcloud secrets delete ${addressesIdentifier( @@ -79,7 +89,7 @@ export async function deleteAgentKeys(agentConfig: AgentConfig) { export async function createAgentKeysIfNotExists( agentConfig: AgentConfig, ) { - const keys = getAllKeys(agentConfig); + const keys = getAllCloudAgentKeys(agentConfig); await Promise.all( keys.map(async (key) => { @@ -99,7 +109,7 @@ export async function rotateKey( role: KEY_ROLE_ENUM, chainName: Chain, ) { - const key = getKey(agentConfig, role, chainName); + const key = getCloudAgentKey(agentConfig, role, chainName); await key.update(); const keyIdentifier = key.identifier; const addresses = await fetchGCPKeyAddresses( @@ -137,11 +147,11 @@ async function persistAddresses( export async function fetchKeysForChain( agentConfig: AgentConfig, chainName: Chain, -): Promise> { +): Promise> { // Get all keys for the chainName. Include keys where chainName is undefined, // which are keys that are not chain-specific but should still be included const keys = await Promise.all( - getAllKeys(agentConfig) + getAllCloudAgentKeys(agentConfig) .filter( (key) => key.chainName === undefined || key.chainName == chainName, ) diff --git a/typescript/infra/src/agents/keys.ts b/typescript/infra/src/agents/keys.ts new file mode 100644 index 000000000..17ca997cd --- /dev/null +++ b/typescript/infra/src/agents/keys.ts @@ -0,0 +1,144 @@ +import { ethers } from 'ethers'; + +import { ChainName } from '@abacus-network/sdk'; + +import { Contexts } from '../../config/contexts'; +import { assertChain, assertContext, assertRole } from '../utils/utils'; + +import { KEY_ROLE_ENUM } from './roles'; + +// Base class to represent keys used to run Abacus agents. +export abstract class BaseAgentKey { + constructor( + public readonly environment: string, + public readonly role: KEY_ROLE_ENUM, + public readonly chainName?: ChainName, + public readonly readonly = true, + ) {} + + abstract get address(): string; +} + +// A read-only representation of a key. +export class ReadOnlyAgentKey extends BaseAgentKey { + constructor( + public environment: string, + public readonly role: KEY_ROLE_ENUM, + public readonly address: string, + public readonly chainName?: ChainName, + ) { + super(environment, role, chainName); + } +} + +// Base class to represent cloud-hosted keys used to run Abacus agents. +export abstract class BaseCloudAgentKey extends BaseAgentKey { + abstract get context(): Contexts; + abstract get identifier(): string; +} + +// Base class to represent cloud-hosted keys for which the current +// process has the credentials. +export abstract class CloudAgentKey extends BaseCloudAgentKey { + constructor( + public readonly environment: string, + public readonly context: Contexts, + public readonly role: KEY_ROLE_ENUM, + public readonly chainName?: ChainName, + public readonly index?: number, + ) { + super(environment, role, chainName, false); + } + + abstract fetch(): Promise; + + abstract createIfNotExists(): Promise; + abstract delete(): Promise; + // Returns new address + abstract update(): Promise; + + abstract getSigner( + provider?: ethers.providers.Provider, + ): Promise; + + serializeAsAddress() { + return { + identifier: this.identifier, + address: this.address, + }; + } +} + +// A read-only representation of a key managed internally. +export class ReadOnlyCloudAgentKey extends BaseCloudAgentKey { + constructor( + public readonly environment: string, + public readonly context: Contexts, + public readonly role: KEY_ROLE_ENUM, + public readonly identifier: string, + public readonly address: string, + public readonly chainName?: ChainName, + public readonly index?: number, + ) { + super(environment, role, chainName); + } + + /** + * Parses the identifier, deriving the environment, role, chain (if any), and index (if any) + * and constructs a ReadOnlyAgentKey. + * @param identifier The "identifier" of the key. This can come in a few different + * flavors, e.g.: + * alias/abacus-testnet2-key-kathy (<-- abacus context, not specific to any chain) + * alias/abacus-testnet2-key-optimismkovan-relayer (<-- abacus context, chain specific) + * alias/abacus-testnet2-key-alfajores-validator-0 (<-- abacus context, chain specific and has an index) + * abacus-dev-key-kathy (<-- same idea as above, but without the `alias/` prefix if it's not AWS-based) + * alias/flowcarbon-testnet2-key-optimismkovan-relayer (<-- flowcarbon context & chain specific, intended to show that there are non-abacus contexts) + * @param address The address of the key. + * @returns A ReadOnlyAgentKey for the provided identifier and address. + */ + static fromSerializedAddress( + identifier: string, + address: string, + ): ReadOnlyCloudAgentKey { + const regex = + /(alias\/)?([a-zA-Z0-9]+)-([a-zA-Z0-9]+)-key-([a-zA-Z0-9]+)-?([a-zA-Z0-9]+)?-?([0-9]+)?/g; + const matches = regex.exec(identifier); + if (!matches) { + throw Error('Invalid identifier'); + } + const context = assertContext(matches[2]); + const environment = matches[3]; + + // If matches[5] is undefined, this key doesn't have a chainName, and matches[4] + // is the role name. + if (matches[5] === undefined) { + return new ReadOnlyCloudAgentKey( + environment, + context, + assertRole(matches[4]), + identifier, + address, + ); + } else if (matches[6] === undefined) { + // If matches[6] is undefined, this key doesn't have an index. + return new ReadOnlyCloudAgentKey( + environment, + context, + assertRole(matches[5]), + identifier, + address, + assertChain(matches[4]), + ); + } else { + return new ReadOnlyCloudAgentKey( + environment, + context, + assertRole(matches[5]), + identifier, + address, + assertChain(matches[4]), + parseInt(matches[6]), + ); + } + } +} diff --git a/typescript/infra/src/config/agent.ts b/typescript/infra/src/config/agent.ts index 21d4ac68b..a810b4114 100644 --- a/typescript/infra/src/config/agent.ts +++ b/typescript/infra/src/config/agent.ts @@ -74,6 +74,7 @@ interface ValidatorSet { interface Validator { address: string; checkpointSyncer: CheckpointSyncerConfig; + readonly?: boolean; } // Validator sets for each chain @@ -299,40 +300,43 @@ export class ChainAgentConfig { this.chainName, ); + // Filter out readonly validator keys, as we do not need to run + // validators for these. return Promise.all( - this.validatorSet.validators.map(async (val, i) => { - let validator: KeyConfig = { - type: KeyType.Hex, - }; - - if (val.checkpointSyncer.type === CheckpointSyncerType.S3) { - const awsUser = new ValidatorAgentAwsUser( - this.agentConfig.environment, - this.agentConfig.context, - this.chainName, - i, - val.checkpointSyncer.region, - val.checkpointSyncer.bucket, - ); - await awsUser.createIfNotExists(); - await awsUser.createBucketIfNotExists(); - - if (this.awsKeys) { - const key = await awsUser.createKeyIfNotExists(this.agentConfig); - validator = key.keyConfig; + this.validatorSet.validators + .filter((val) => !val.readonly) + .map(async (val, i) => { + let validator: KeyConfig = { + type: KeyType.Hex, + }; + if (val.checkpointSyncer.type === CheckpointSyncerType.S3) { + const awsUser = new ValidatorAgentAwsUser( + this.agentConfig.environment, + this.agentConfig.context, + this.chainName, + i, + val.checkpointSyncer.region, + val.checkpointSyncer.bucket, + ); + await awsUser.createIfNotExists(); + await awsUser.createBucketIfNotExists(); + + if (this.awsKeys) { + const key = await awsUser.createKeyIfNotExists(this.agentConfig); + validator = key.keyConfig; + } + } else { + console.warn( + `Validator ${val.address}'s checkpoint syncer is not S3-based. Be sure this is a non-k8s-based environment!`, + ); } - } else { - console.warn( - `Validator ${val.address}'s checkpoint syncer is not S3-based. Be sure this is a non-k8s-based environment!`, - ); - } - - return { - ...baseConfig, - checkpointSyncer: val.checkpointSyncer, - validator, - }; - }), + + return { + ...baseConfig, + checkpointSyncer: val.checkpointSyncer, + validator, + }; + }), ); } diff --git a/typescript/infra/test/agents.test.ts b/typescript/infra/test/agents.test.ts index a1637de71..5aa508cd3 100644 --- a/typescript/infra/test/agents.test.ts +++ b/typescript/infra/test/agents.test.ts @@ -1,12 +1,12 @@ import { expect } from 'chai'; import { Contexts } from '../config/contexts'; -import { ReadOnlyAgentKey } from '../src/agents/agent'; import { AgentAwsKey } from '../src/agents/aws'; import { AgentGCPKey } from '../src/agents/gcp'; +import { ReadOnlyCloudAgentKey } from '../src/agents/keys'; import { KEY_ROLE_ENUM } from '../src/agents/roles'; -describe('ReadOnlyAgentKey', () => { +describe('ReadOnlyCloudAgentKey', () => { describe('fromSerializedAddress', () => { it('correctly parses identifiers', () => { const addressZero = '0x0000000000000000000000000000000000000000'; @@ -42,7 +42,7 @@ describe('ReadOnlyAgentKey', () => { for (const testKey of testKeys) { const identifier = testKey.identifier; - const readOnly = ReadOnlyAgentKey.fromSerializedAddress( + const readOnly = ReadOnlyCloudAgentKey.fromSerializedAddress( identifier, addressZero, );