Relayer funder can fund multiple contexts (#830)

* wip

* Fund relayers appropriately

* Relayer funder works with many contexts

* Self nits

* Prettier
trevor/deploy-relayer-funder-multi-context
Trevor Porter 2 years ago committed by GitHub
parent 436b10952e
commit 93280059d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      typescript/infra/config/environments/mainnet/funding.ts
  2. 3
      typescript/infra/config/environments/testnet2/funding.ts
  3. 10
      typescript/infra/helm/relayer-funder/templates/addresses-external-secret.yaml
  4. 8
      typescript/infra/helm/relayer-funder/templates/cron-job.yaml
  5. 3
      typescript/infra/helm/relayer-funder/values.yaml
  6. 336
      typescript/infra/scripts/funding/fund-relayers-from-deployer.ts
  7. 10
      typescript/infra/scripts/utils.ts
  8. 4
      typescript/infra/src/config/funding.ts
  9. 6
      typescript/infra/src/funding/deploy-relayer-funder.ts

@ -1,4 +1,5 @@
import { RelayerFunderConfig } from '../../../src/config/funding'; import { RelayerFunderConfig } from '../../../src/config/funding';
import { Contexts } from '../../contexts';
import { environment } from './chains'; import { environment } from './chains';
@ -11,4 +12,6 @@ export const relayerFunderConfig: RelayerFunderConfig = {
namespace: environment, namespace: environment,
prometheusPushGateway: prometheusPushGateway:
'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091', 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091',
contextFundingFrom: Contexts.Abacus,
contextsToFund: [Contexts.Abacus],
}; };

@ -1,4 +1,5 @@
import { RelayerFunderConfig } from '../../../src/config/funding'; import { RelayerFunderConfig } from '../../../src/config/funding';
import { Contexts } from '../../contexts';
import { environment } from './chains'; import { environment } from './chains';
@ -11,4 +12,6 @@ export const relayerFunderConfig: RelayerFunderConfig = {
namespace: environment, namespace: environment,
prometheusPushGateway: prometheusPushGateway:
'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091', 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091',
contextFundingFrom: Contexts.Abacus,
contextsToFund: [Contexts.Abacus],
}; };

@ -20,8 +20,12 @@ spec:
annotations: annotations:
update-on-redeploy: "{{ now }}" update-on-redeploy: "{{ now }}"
data: data:
addresses.json: {{ print "'{{ .addresses | toString }}'" }} {{- range .Values.abacus.contextsToFund }}
{{ . }}-addresses.json: {{ printf "'{{ .%s_addresses | toString }}'" . }}
{{- end }}
data: data:
- secretKey: addresses {{- range .Values.abacus.contextsToFund }}
- secretKey: {{ . }}_addresses
remoteRef: remoteRef:
key: {{ printf "abacus-%s-key-addresses" .Values.abacus.runEnv }} key: {{ printf "%s-%s-key-addresses" . $.Values.abacus.runEnv }}
{{- end }}

@ -22,10 +22,12 @@ spec:
- ./typescript/infra/scripts/funding/fund-relayers-from-deployer.ts - ./typescript/infra/scripts/funding/fund-relayers-from-deployer.ts
- -e - -e
- {{ .Values.abacus.runEnv }} - {{ .Values.abacus.runEnv }}
- -f
- /addresses-secret/addresses.json
- --context - --context
- abacus - {{ .Values.abacus.contextFundingFrom }}
{{- range .Values.abacus.contextsToFund }}
- -f
- /addresses-secret/{{ . }}-addresses.json
{{- end }}
env: env:
- name: PROMETHEUS_PUSH_GATEWAY - name: PROMETHEUS_PUSH_GATEWAY
value: {{ .Values.infra.prometheusPushGateway }} value: {{ .Values.infra.prometheusPushGateway }}

@ -5,6 +5,9 @@ abacus:
runEnv: testnet2 runEnv: testnet2
# Used for fetching secrets # Used for fetching secrets
chains: [] chains: []
contextFundingFrom: abacus
contextsToFund:
- abacus
cronjob: cronjob:
schedule: "*/10 * * * *" # Every 10 minutes schedule: "*/10 * * * *" # Every 10 minutes
successfulJobsHistoryLimit: 1 successfulJobsHistoryLimit: 1

@ -2,18 +2,24 @@ import { ethers } from 'ethers';
import { Gauge, Registry } from 'prom-client'; import { Gauge, Registry } from 'prom-client';
import { format } from 'util'; 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 { error, log } from '@abacus-network/utils';
import { Contexts } from '../../config/contexts';
import { AgentKey, ReadOnlyAgentKey } from '../../src/agents/agent'; import { AgentKey, ReadOnlyAgentKey } from '../../src/agents/agent';
import { getRelayerKeys } from '../../src/agents/key-utils'; import { getRelayerKeys } from '../../src/agents/key-utils';
import { KEY_ROLE_ENUM } from '../../src/agents/roles'; import { KEY_ROLE_ENUM } from '../../src/agents/roles';
import { submitMetrics } from '../../src/utils/metrics'; import { submitMetrics } from '../../src/utils/metrics';
import { readJSONAtPath } from '../../src/utils/utils'; import { assertContext, readJSONAtPath } from '../../src/utils/utils';
import { import {
assertEnvironment, assertEnvironment,
getAgentConfig,
getArgs, getArgs,
getContextAgentConfig,
getCoreEnvironmentConfig, getCoreEnvironmentConfig,
} from '../utils'; } from '../utils';
@ -24,6 +30,7 @@ const constMetricLabels = {
}; };
const metricsRegister = new Registry(); const metricsRegister = new Registry();
const walletBalanceGauge = new Gauge({ const walletBalanceGauge = new Gauge({
// Mirror the rust/ethers-prometheus `wallet_balance` gauge metric. // Mirror the rust/ethers-prometheus `wallet_balance` gauge metric.
name: 'abacus_wallet_balance', name: 'abacus_wallet_balance',
@ -68,151 +75,246 @@ const desiredBalancePerChain: CompleteChainMap<string> = {
test3: '0', test3: '0',
}; };
async function fundRelayer( // Funds relayer addresses for multiple contexts from the deployer key of the context
chainConnection: ChainConnection, // specified via the `--context` flag.
relayer: AgentKey, // There are two ways to configure this script so that relayer addresses are known.
desiredBalance: string, // 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
const currentBalance = await chainConnection.provider.getBalance( // are all for the same context. This will avoid requiring any sort of GCP/AWS credentials for
relayer.address, // fetching addresses from the keys themselves.
); // Alternatively, using `--contexts-to-fund` will fetch relayer addresses from GCP/AWS, which
const desiredBalanceEther = ethers.utils.parseUnits(desiredBalance, 'ether'); // requires credentials.
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),
),
});
}
async function main() { async function main() {
const argv = await getArgs() const argv = await getArgs()
.alias('f', 'addresses-file') .string('f')
.array('f')
.alias('f', 'address-files')
.describe( .describe(
'f', '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); const environment = assertEnvironment(argv.e as string);
constMetricLabels.abacus_deployment = environment; constMetricLabels.abacus_deployment = environment;
const config = getCoreEnvironmentConfig(environment); const config = getCoreEnvironmentConfig(environment);
const multiProvider = await config.getMultiProvider(); const multiProvider = await config.getMultiProvider();
const agentConfig = await getContextAgentConfig(config);
const relayerKeys = argv.f const contextRelayerFunders = argv.f
? getRelayerKeysFromSerializedAddressFile(argv.f) ? argv.f.map((path) =>
: getRelayerKeys(agentConfig); ContextRelayerFunder.fromSerializedAddressFile(multiProvider, path),
)
: argv.contextsToFund!.map((context) =>
ContextRelayerFunder.fromSerializedAddressFile(multiProvider, context),
);
const chains = relayerKeys.map((key) => key.chainName!);
let failureOccurred = false; 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) { // Funds relayers for a single context
const chainConnection = multiProvider.getChainConnection(chain); class ContextRelayerFunder {
public readonly chains: ChainName[];
const desiredBalance = desiredBalancePerChain[chain]; constructor(
const funderAddress = await chainConnection.getAddress(); public readonly multiProvider: MultiProvider<any>,
public readonly keys: AgentKey[],
public readonly context: Contexts,
) {
this.chains = keys.map((key) => key.chainName!);
}
log('Funding relayers on chain...', { static fromSerializedAddressFile(
chain, multiProvider: MultiProvider<any>,
funder: { path: string,
address: funderAddress, ) {
balance: ethers.utils.formatEther( log('Reading identifiers and addresses from file', {
await chainConnection.signer!.getBalance(), 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( return new ContextRelayerFunder(multiProvider, keys, context);
(key) => key.chainName !== chain, }
)) {
await relayerKey.fetch(); // The keys here are not ReadOnlyAgentKeys, instead they are AgentGCPKey or AgentAWSKeys,
try { // which require credentials to fetch. If you want to avoid requiring credentials, use
await fundRelayer(chainConnection, relayerKey, desiredBalance); // fromSerializedAddressFile instead.
} catch (err) { static async fromContext(
error('Error funding relayer', { multiProvider: MultiProvider<any>,
relayer: relayerKeyInfo(relayerKey), context: Contexts,
error: err, ) {
}); const agentConfig = await getAgentConfig(context);
failureOccurred = true; 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 return failureOccurred;
.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()),
),
);
} }
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) { const minDelta = desiredBalanceEther
error('At least one failure occurred when funding relayers'); .mul(MIN_DELTA_NUMERATOR)
process.exit(1); .div(MIN_DELTA_DENOMINATOR);
}
}
function getRelayerKeysFromSerializedAddressFile(path: string): AgentKey[] { const relayerInfo = relayerKeyInfo(key);
log('Reading keys from file', {
path, if (delta.gt(minDelta)) {
}); log('Sending relayer funds...', {
// Should be an array of { identifier: '', address: '' } relayer: relayerInfo,
const idAndAddresses = readJSONAtPath(path); amount: ethers.utils.formatEther(delta),
context: this.context,
return idAndAddresses chain,
.map((idAndAddress: any) => });
ReadOnlyAgentKey.fromSerializedAddress(
idAndAddress.identifier, const tx = await chainConnection.signer!.sendTransaction({
idAndAddress.address, to: key.address,
), value: delta,
) ...chainConnection.overrides,
.filter((key: AgentKey) => key.role === KEY_ROLE_ENUM.Relayer); });
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) { function relayerKeyInfo(relayerKey: AgentKey) {
return { return {
context: relayerKey.context,
address: relayerKey.address, address: relayerKey.address,
identifier: relayerKey.identifier, identifier: relayerKey.identifier,
chain: relayerKey.chainName, originChain: relayerKey.chainName,
}; };
} }

@ -64,13 +64,21 @@ export async function getContext(): Promise<Contexts> {
return assertContext(argv.context!); return assertContext(argv.context!);
} }
// Gets the agent config for the context that has been specified via yargs.
export async function getContextAgentConfig<Chain extends ChainName>( export async function getContextAgentConfig<Chain extends ChainName>(
coreEnvironmentConfig?: CoreEnvironmentConfig<Chain>, coreEnvironmentConfig?: CoreEnvironmentConfig<Chain>,
) {
return getAgentConfig(await getContext(), coreEnvironmentConfig);
}
// Gets the agent config of a specific context.
export async function getAgentConfig<Chain extends ChainName>(
context: Contexts,
coreEnvironmentConfig?: CoreEnvironmentConfig<Chain>,
) { ) {
const coreConfig = coreEnvironmentConfig const coreConfig = coreEnvironmentConfig
? coreEnvironmentConfig ? coreEnvironmentConfig
: await getEnvironmentConfig(); : await getEnvironmentConfig();
const context = await getContext();
const agentConfig = coreConfig.agents[context]; const agentConfig = coreConfig.agents[context];
if (!agentConfig) { if (!agentConfig) {
throw Error( throw Error(

@ -1,8 +1,12 @@
import { Contexts } from '../../config/contexts';
import { DockerConfig } from './agent'; import { DockerConfig } from './agent';
export interface RelayerFunderConfig { export interface RelayerFunderConfig {
docker: DockerConfig; docker: DockerConfig;
cronSchedule: string; cronSchedule: string;
namespace: string; namespace: string;
contextFundingFrom: Contexts;
contextsToFund: Contexts[];
prometheusPushGateway: string; prometheusPushGateway: string;
} }

@ -16,6 +16,9 @@ export function runRelayerFunderHelmCommand<Chain extends ChainName>(
`helm ${helmCommand} relayer-funder ./helm/relayer-funder --namespace ${ `helm ${helmCommand} relayer-funder ./helm/relayer-funder --namespace ${
relayerFunderConfig.namespace relayerFunderConfig.namespace
} ${values.join(' ')}`, } ${values.join(' ')}`,
{},
false,
true,
); );
} }
@ -29,7 +32,10 @@ function getRelayerFunderHelmValues<Chain extends ChainName>(
}, },
abacus: { abacus: {
runEnv: agentConfig.environment, runEnv: agentConfig.environment,
// Only used for fetching RPC urls as env vars
chains: agentConfig.contextChainNames, chains: agentConfig.contextChainNames,
contextFundingFrom: relayerFunderConfig.contextFundingFrom,
contextsToFund: relayerFunderConfig.contextsToFund,
}, },
image: { image: {
repository: relayerFunderConfig.docker.repo, repository: relayerFunderConfig.docker.repo,

Loading…
Cancel
Save