From 93280059d6bb2cfd0be2336db8b6d4fa8663ad67 Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Wed, 3 Aug 2022 12:57:39 +0100 Subject: [PATCH] Relayer funder can fund multiple contexts (#830) * wip * Fund relayers appropriately * Relayer funder works with many contexts * Self nits * Prettier --- .../config/environments/mainnet/funding.ts | 3 + .../config/environments/testnet2/funding.ts | 3 + .../templates/addresses-external-secret.yaml | 10 +- .../relayer-funder/templates/cron-job.yaml | 8 +- .../infra/helm/relayer-funder/values.yaml | 3 + .../funding/fund-relayers-from-deployer.ts | 336 ++++++++++++------ typescript/infra/scripts/utils.ts | 10 +- typescript/infra/src/config/funding.ts | 4 + .../src/funding/deploy-relayer-funder.ts | 6 + 9 files changed, 259 insertions(+), 124 deletions(-) diff --git a/typescript/infra/config/environments/mainnet/funding.ts b/typescript/infra/config/environments/mainnet/funding.ts index ab8723670..e2425efed 100644 --- a/typescript/infra/config/environments/mainnet/funding.ts +++ b/typescript/infra/config/environments/mainnet/funding.ts @@ -1,4 +1,5 @@ import { RelayerFunderConfig } from '../../../src/config/funding'; +import { Contexts } from '../../contexts'; import { environment } from './chains'; @@ -11,4 +12,6 @@ export const relayerFunderConfig: RelayerFunderConfig = { namespace: environment, prometheusPushGateway: 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091', + contextFundingFrom: Contexts.Abacus, + contextsToFund: [Contexts.Abacus], }; diff --git a/typescript/infra/config/environments/testnet2/funding.ts b/typescript/infra/config/environments/testnet2/funding.ts index ab8723670..e2425efed 100644 --- a/typescript/infra/config/environments/testnet2/funding.ts +++ b/typescript/infra/config/environments/testnet2/funding.ts @@ -1,4 +1,5 @@ import { RelayerFunderConfig } from '../../../src/config/funding'; +import { Contexts } from '../../contexts'; import { environment } from './chains'; @@ -11,4 +12,6 @@ export const relayerFunderConfig: RelayerFunderConfig = { namespace: environment, prometheusPushGateway: 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091', + contextFundingFrom: Contexts.Abacus, + contextsToFund: [Contexts.Abacus], }; diff --git a/typescript/infra/helm/relayer-funder/templates/addresses-external-secret.yaml b/typescript/infra/helm/relayer-funder/templates/addresses-external-secret.yaml index d3c0021ae..61c207e14 100644 --- a/typescript/infra/helm/relayer-funder/templates/addresses-external-secret.yaml +++ b/typescript/infra/helm/relayer-funder/templates/addresses-external-secret.yaml @@ -20,8 +20,12 @@ spec: annotations: update-on-redeploy: "{{ now }}" data: - addresses.json: {{ print "'{{ .addresses | toString }}'" }} +{{- range .Values.abacus.contextsToFund }} + {{ . }}-addresses.json: {{ printf "'{{ .%s_addresses | toString }}'" . }} +{{- end }} data: - - secretKey: addresses +{{- range .Values.abacus.contextsToFund }} + - secretKey: {{ . }}_addresses remoteRef: - key: {{ printf "abacus-%s-key-addresses" .Values.abacus.runEnv }} + key: {{ printf "%s-%s-key-addresses" . $.Values.abacus.runEnv }} +{{- end }} diff --git a/typescript/infra/helm/relayer-funder/templates/cron-job.yaml b/typescript/infra/helm/relayer-funder/templates/cron-job.yaml index 17a9bcc1f..2439d32fc 100644 --- a/typescript/infra/helm/relayer-funder/templates/cron-job.yaml +++ b/typescript/infra/helm/relayer-funder/templates/cron-job.yaml @@ -22,10 +22,12 @@ spec: - ./typescript/infra/scripts/funding/fund-relayers-from-deployer.ts - -e - {{ .Values.abacus.runEnv }} - - -f - - /addresses-secret/addresses.json - --context - - abacus + - {{ .Values.abacus.contextFundingFrom }} +{{- range .Values.abacus.contextsToFund }} + - -f + - /addresses-secret/{{ . }}-addresses.json +{{- end }} env: - name: PROMETHEUS_PUSH_GATEWAY value: {{ .Values.infra.prometheusPushGateway }} diff --git a/typescript/infra/helm/relayer-funder/values.yaml b/typescript/infra/helm/relayer-funder/values.yaml index 39b404432..5c6fa5251 100644 --- a/typescript/infra/helm/relayer-funder/values.yaml +++ b/typescript/infra/helm/relayer-funder/values.yaml @@ -5,6 +5,9 @@ abacus: runEnv: testnet2 # Used for fetching secrets chains: [] + contextFundingFrom: abacus + contextsToFund: + - abacus cronjob: schedule: "*/10 * * * *" # Every 10 minutes successfulJobsHistoryLimit: 1 diff --git a/typescript/infra/scripts/funding/fund-relayers-from-deployer.ts b/typescript/infra/scripts/funding/fund-relayers-from-deployer.ts index c703062c5..cffece150 100644 --- a/typescript/infra/scripts/funding/fund-relayers-from-deployer.ts +++ b/typescript/infra/scripts/funding/fund-relayers-from-deployer.ts @@ -2,18 +2,24 @@ import { ethers } from 'ethers'; import { Gauge, Registry } from 'prom-client'; import { format } from 'util'; -import { ChainConnection, CompleteChainMap } from '@abacus-network/sdk'; +import { + ChainConnection, + ChainName, + CompleteChainMap, + MultiProvider, +} from '@abacus-network/sdk'; import { error, log } from '@abacus-network/utils'; +import { Contexts } from '../../config/contexts'; import { AgentKey, ReadOnlyAgentKey } from '../../src/agents/agent'; import { getRelayerKeys } from '../../src/agents/key-utils'; import { KEY_ROLE_ENUM } from '../../src/agents/roles'; import { submitMetrics } from '../../src/utils/metrics'; -import { readJSONAtPath } from '../../src/utils/utils'; +import { assertContext, readJSONAtPath } from '../../src/utils/utils'; import { assertEnvironment, + getAgentConfig, getArgs, - getContextAgentConfig, getCoreEnvironmentConfig, } from '../utils'; @@ -24,6 +30,7 @@ const constMetricLabels = { }; const metricsRegister = new Registry(); + const walletBalanceGauge = new Gauge({ // Mirror the rust/ethers-prometheus `wallet_balance` gauge metric. name: 'abacus_wallet_balance', @@ -68,151 +75,246 @@ const desiredBalancePerChain: CompleteChainMap = { test3: '0', }; -async function fundRelayer( - chainConnection: ChainConnection, - relayer: AgentKey, - desiredBalance: string, -) { - const currentBalance = await chainConnection.provider.getBalance( - relayer.address, - ); - const desiredBalanceEther = ethers.utils.parseUnits(desiredBalance, 'ether'); - const delta = desiredBalanceEther.sub(currentBalance); - - const minDelta = desiredBalanceEther - .mul(MIN_DELTA_NUMERATOR) - .div(MIN_DELTA_DENOMINATOR); - - const relayerInfo = relayerKeyInfo(relayer); - - if (delta.gt(minDelta)) { - log('Sending relayer funds...', { - relayer: relayerInfo, - amount: ethers.utils.formatEther(delta), - }); - const tx = await chainConnection.signer!.sendTransaction({ - to: relayer.address, - value: delta, - ...chainConnection.overrides, - }); - log('Sent transaction', { - relayer: relayerInfo, - txUrl: chainConnection.getTxUrl(tx), - }); - const receipt = await tx.wait(chainConnection.confirmations); - log('Got transaction receipt', { - relayer: relayerInfo, - receipt, - }); - } - - log('Relayer balance', { - relayer: relayerInfo, - balance: ethers.utils.formatEther( - await chainConnection.provider.getBalance(relayer.address), - ), - }); -} - +// Funds relayer addresses for multiple contexts from the deployer key of the context +// specified via the `--context` flag. +// There are two ways to configure this script so that relayer addresses are known. +// You can pass in files using `-f`, which are expected to each be JSON arrays of objects +// of the form { identifier: '..', address: '..' }, where the keys described in one file +// are all for the same context. This will avoid requiring any sort of GCP/AWS credentials for +// fetching addresses from the keys themselves. +// Alternatively, using `--contexts-to-fund` will fetch relayer addresses from GCP/AWS, which +// requires credentials. async function main() { const argv = await getArgs() - .alias('f', 'addresses-file') + .string('f') + .array('f') + .alias('f', 'address-files') .describe( 'f', - 'File continaining a JSON array of identifier and address objects', + 'Files each containing JSON arrays of identifier and address objects for a context', ) - .string('f').argv; + .string('contexts-to-fund') + .array('contexts-to-fund') + .describe( + 'contexts-to-fund', + 'Contexts to fund relayers for. If specified, relayer addresses are fetched from GCP/AWS and require sufficient credentials.', + ) + .coerce('contexts-to-fund', (contexts: string[]) => { + return contexts.map(assertContext); + }) + // Only one of the two methods for getting relayer addresses + .conflicts('f', 'contexts-to-fund').argv; const environment = assertEnvironment(argv.e as string); constMetricLabels.abacus_deployment = environment; const config = getCoreEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); - const agentConfig = await getContextAgentConfig(config); - const relayerKeys = argv.f - ? getRelayerKeysFromSerializedAddressFile(argv.f) - : getRelayerKeys(agentConfig); + const contextRelayerFunders = argv.f + ? argv.f.map((path) => + ContextRelayerFunder.fromSerializedAddressFile(multiProvider, path), + ) + : argv.contextsToFund!.map((context) => + ContextRelayerFunder.fromSerializedAddressFile(multiProvider, context), + ); - const chains = relayerKeys.map((key) => key.chainName!); let failureOccurred = false; + for (const relayerFunder of contextRelayerFunders) { + const failure = await relayerFunder.fundRelayersOnAllChains(); + if (failure) { + failureOccurred = true; + } + } + + await submitMetrics(metricsRegister, 'relayer-funder'); + + if (failureOccurred) { + error('At least one failure occurred when funding relayers'); + process.exit(1); + } +} - for (const chain of chains) { - const chainConnection = multiProvider.getChainConnection(chain); +// Funds relayers for a single context +class ContextRelayerFunder { + public readonly chains: ChainName[]; - const desiredBalance = desiredBalancePerChain[chain]; - const funderAddress = await chainConnection.getAddress(); + constructor( + public readonly multiProvider: MultiProvider, + public readonly keys: AgentKey[], + public readonly context: Contexts, + ) { + this.chains = keys.map((key) => key.chainName!); + } - log('Funding relayers on chain...', { - chain, - funder: { - address: funderAddress, - balance: ethers.utils.formatEther( - await chainConnection.signer!.getBalance(), + static fromSerializedAddressFile( + multiProvider: MultiProvider, + path: string, + ) { + log('Reading identifiers and addresses from file', { + path, + }); + const idsAndAddresses = readJSONAtPath(path); + const keys: AgentKey[] = idsAndAddresses + .map((idAndAddress: any) => + ReadOnlyAgentKey.fromSerializedAddress( + idAndAddress.identifier, + idAndAddress.address, ), - desiredRelayerBalance: desiredBalance, - }, + ) + .filter((key: AgentKey) => key.role === KEY_ROLE_ENUM.Relayer); + + const context = keys[0].context; + // Ensure all keys have the same context, just to be safe + keys.forEach((key) => { + if (key.context !== context) { + throw Error( + `Expected all keys at path ${path} to have context ${context}, found ${key.context}`, + ); + } + }); + + log('Read keys for context from file', { + path, + keyCount: keys.length, + context, }); - for (const relayerKey of relayerKeys.filter( - (key) => key.chainName !== chain, - )) { - await relayerKey.fetch(); - try { - await fundRelayer(chainConnection, relayerKey, desiredBalance); - } catch (err) { - error('Error funding relayer', { - relayer: relayerKeyInfo(relayerKey), - error: err, - }); - failureOccurred = true; + return new ContextRelayerFunder(multiProvider, keys, context); + } + + // The keys here are not ReadOnlyAgentKeys, instead they are AgentGCPKey or AgentAWSKeys, + // which require credentials to fetch. If you want to avoid requiring credentials, use + // fromSerializedAddressFile instead. + static async fromContext( + multiProvider: MultiProvider, + context: Contexts, + ) { + const agentConfig = await getAgentConfig(context); + return new ContextRelayerFunder( + multiProvider, + getRelayerKeys(agentConfig), + context, + ); + } + + // Funds the relayers on all the chains found in `this.chains` + async fundRelayersOnAllChains() { + let failureOccurred = false; + + for (const chain of this.chains) { + const chainConnection = this.multiProvider.getChainConnection(chain); + + const desiredBalance = desiredBalancePerChain[chain]; + const funderAddress = await chainConnection.getAddress(); + + log('Funding relayers on chain...', { + chain, + funder: { + address: funderAddress, + balance: ethers.utils.formatEther( + await chainConnection.signer!.getBalance(), + ), + desiredRelayerBalance: desiredBalance, + }, + context: this.context, + }); + + for (const key of this.keys.filter((k) => k.chainName !== chain)) { + await key.fetch(); + try { + await this.fundRelayerIfRequired( + chainConnection, + chain, + key, + desiredBalance, + ); + } catch (err) { + error('Error funding relayer', { + relayer: relayerKeyInfo(key), + context: this.context, + error: err, + }); + failureOccurred = true; + } } + + walletBalanceGauge + .labels({ + chain, + wallet_address: funderAddress ?? 'unknown', + wallet_name: 'relayer-funder', + token_symbol: 'Native', + token_name: 'Native', + ...constMetricLabels, + }) + .set( + parseFloat( + ethers.utils.formatEther( + await chainConnection.signer!.getBalance(), + ), + ), + ); } - walletBalanceGauge - .labels({ - chain, - wallet_address: funderAddress ?? 'unknown', - wallet_name: 'relayer-funder', - token_symbol: 'Native', - token_name: 'Native', - ...constMetricLabels, - }) - .set( - parseFloat( - ethers.utils.formatEther(await chainConnection.signer!.getBalance()), - ), - ); + return failureOccurred; } - await submitMetrics(metricsRegister, 'relayer-funder'); + private async fundRelayerIfRequired( + chainConnection: ChainConnection, + chain: ChainName, + key: AgentKey, + desiredBalance: string, + ) { + const currentBalance = await chainConnection.provider.getBalance( + key.address, + ); + const desiredBalanceEther = ethers.utils.parseUnits( + desiredBalance, + 'ether', + ); + const delta = desiredBalanceEther.sub(currentBalance); - if (failureOccurred) { - error('At least one failure occurred when funding relayers'); - process.exit(1); - } -} + const minDelta = desiredBalanceEther + .mul(MIN_DELTA_NUMERATOR) + .div(MIN_DELTA_DENOMINATOR); -function getRelayerKeysFromSerializedAddressFile(path: string): AgentKey[] { - log('Reading keys from file', { - path, - }); - // Should be an array of { identifier: '', address: '' } - const idAndAddresses = readJSONAtPath(path); - - return idAndAddresses - .map((idAndAddress: any) => - ReadOnlyAgentKey.fromSerializedAddress( - idAndAddress.identifier, - idAndAddress.address, - ), - ) - .filter((key: AgentKey) => key.role === KEY_ROLE_ENUM.Relayer); + const relayerInfo = relayerKeyInfo(key); + + if (delta.gt(minDelta)) { + log('Sending relayer funds...', { + relayer: relayerInfo, + amount: ethers.utils.formatEther(delta), + context: this.context, + chain, + }); + + const tx = await chainConnection.signer!.sendTransaction({ + to: key.address, + value: delta, + ...chainConnection.overrides, + }); + log('Sent transaction', { + relayer: relayerInfo, + txUrl: chainConnection.getTxUrl(tx), + context: this.context, + chain, + }); + const receipt = await tx.wait(chainConnection.confirmations); + log('Got transaction receipt', { + relayer: relayerInfo, + receipt, + context: this.context, + chain, + }); + } + } } function relayerKeyInfo(relayerKey: AgentKey) { return { + context: relayerKey.context, address: relayerKey.address, identifier: relayerKey.identifier, - chain: relayerKey.chainName, + originChain: relayerKey.chainName, }; } diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index e0e1a3399..cf5377f2b 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -64,13 +64,21 @@ export async function getContext(): Promise { return assertContext(argv.context!); } +// Gets the agent config for the context that has been specified via yargs. export async function getContextAgentConfig( coreEnvironmentConfig?: CoreEnvironmentConfig, +) { + return getAgentConfig(await getContext(), coreEnvironmentConfig); +} + +// Gets the agent config of a specific context. +export async function getAgentConfig( + context: Contexts, + coreEnvironmentConfig?: CoreEnvironmentConfig, ) { const coreConfig = coreEnvironmentConfig ? coreEnvironmentConfig : await getEnvironmentConfig(); - const context = await getContext(); const agentConfig = coreConfig.agents[context]; if (!agentConfig) { throw Error( diff --git a/typescript/infra/src/config/funding.ts b/typescript/infra/src/config/funding.ts index ae24f7181..8660b8287 100644 --- a/typescript/infra/src/config/funding.ts +++ b/typescript/infra/src/config/funding.ts @@ -1,8 +1,12 @@ +import { Contexts } from '../../config/contexts'; + import { DockerConfig } from './agent'; export interface RelayerFunderConfig { docker: DockerConfig; cronSchedule: string; namespace: string; + contextFundingFrom: Contexts; + contextsToFund: Contexts[]; prometheusPushGateway: string; } diff --git a/typescript/infra/src/funding/deploy-relayer-funder.ts b/typescript/infra/src/funding/deploy-relayer-funder.ts index 04195baec..e56d7f7b8 100644 --- a/typescript/infra/src/funding/deploy-relayer-funder.ts +++ b/typescript/infra/src/funding/deploy-relayer-funder.ts @@ -16,6 +16,9 @@ export function runRelayerFunderHelmCommand( `helm ${helmCommand} relayer-funder ./helm/relayer-funder --namespace ${ relayerFunderConfig.namespace } ${values.join(' ')}`, + {}, + false, + true, ); } @@ -29,7 +32,10 @@ function getRelayerFunderHelmValues( }, abacus: { runEnv: agentConfig.environment, + // Only used for fetching RPC urls as env vars chains: agentConfig.contextChainNames, + contextFundingFrom: relayerFunderConfig.contextFundingFrom, + contextsToFund: relayerFunderConfig.contextsToFund, }, image: { repository: relayerFunderConfig.docker.repo,