Add support for readonly validators in infra (#992)

pull/1042/head
Asa Oines 2 years ago committed by GitHub
parent 3cd8982703
commit 10273126fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 38
      typescript/infra/scripts/funding/fund-keys-from-deployer.ts
  2. 4
      typescript/infra/scripts/get-key-addresses.ts
  3. 8
      typescript/infra/scripts/utils.ts
  4. 141
      typescript/infra/src/agents/agent.ts
  5. 5
      typescript/infra/src/agents/aws/key.ts
  6. 5
      typescript/infra/src/agents/gcp.ts
  7. 52
      typescript/infra/src/agents/key-utils.ts
  8. 144
      typescript/infra/src/agents/keys.ts
  9. 68
      typescript/infra/src/config/agent.ts
  10. 6
      typescript/infra/test/agents.test.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<any>,
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<boolean> {
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,
};
}

@ -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) => {

@ -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 extends ChainName>(
chain: Chain,
role: KEY_ROLE_ENUM,
index?: number,
): Promise<AgentKey> {
): Promise<CloudAgentKey> {
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<Chain extends ChainName>(

@ -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<void>;
abstract createIfNotExists(): Promise<void>;
abstract delete(): Promise<void>;
// Returns new address
abstract update(): Promise<string>;
abstract getSigner(
provider?: ethers.providers.Provider,
): Promise<ethers.Signer>;
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<void> {
// No-op
}
async createIfNotExists(): Promise<void> {
throw Error('Not supported');
}
async delete(): Promise<void> {
throw Error('Not supported');
}
async update(): Promise<string> {
throw Error('Not supported');
}
async getSigner(): Promise<ethers.Signer> {
throw Error('Not supported');
}
}
export function isValidatorKey(role: string) {
return role === KEY_ROLE_ENUM.Validator;
}

@ -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 };

@ -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,

@ -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<Chain extends ChainName>(
export function getCloudAgentKey<Chain extends ChainName>(
agentConfig: AgentConfig<Chain>,
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<Chain extends ChainName>(
}
}
export function getValidatorKeys(
export function getValidatorCloudAgentKeys(
agentConfig: AgentConfig<any>,
): Array<AgentKey> {
): Array<CloudAgentKey> {
// 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<any>): Array<AgentKey> {
export function getRelayerCloudAgentKeys(
agentConfig: AgentConfig<any>,
): Array<CloudAgentKey> {
return agentConfig.contextChainNames.map((chainName) =>
getKey(agentConfig, KEY_ROLE_ENUM.Relayer, chainName),
getCloudAgentKey(agentConfig, KEY_ROLE_ENUM.Relayer, chainName),
);
}
export function getAllKeys(agentConfig: AgentConfig<any>): Array<AgentKey> {
export function getAllCloudAgentKeys(
agentConfig: AgentConfig<any>,
): Array<CloudAgentKey> {
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<any>) {
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<any>) {
export async function createAgentKeysIfNotExists(
agentConfig: AgentConfig<any>,
) {
const keys = getAllKeys(agentConfig);
const keys = getAllCloudAgentKeys(agentConfig);
await Promise.all(
keys.map(async (key) => {
@ -99,7 +109,7 @@ export async function rotateKey<Chain extends ChainName>(
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<Chain extends ChainName>(
agentConfig: AgentConfig<Chain>,
chainName: Chain,
): Promise<Record<string, AgentKey>> {
): Promise<Record<string, CloudAgentKey>> {
// 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,
)

@ -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<void>;
abstract createIfNotExists(): Promise<void>;
abstract delete(): Promise<void>;
// Returns new address
abstract update(): Promise<string>;
abstract getSigner(
provider?: ethers.providers.Provider,
): Promise<ethers.Signer>;
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]),
);
}
}
}

@ -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<Chain extends ChainName> {
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,
};
}),
);
}

@ -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,
);

Loading…
Cancel
Save