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. 274
      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 { 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],
};

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

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

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

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

@ -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,76 +75,134 @@ const desiredBalancePerChain: CompleteChainMap<string> = {
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;
}
}
for (const chain of chains) {
const chainConnection = multiProvider.getChainConnection(chain);
await submitMetrics(metricsRegister, 'relayer-funder');
if (failureOccurred) {
error('At least one failure occurred when funding relayers');
process.exit(1);
}
}
// Funds relayers for a single context
class ContextRelayerFunder {
public readonly chains: ChainName[];
constructor(
public readonly multiProvider: MultiProvider<any>,
public readonly keys: AgentKey[],
public readonly context: Contexts,
) {
this.chains = keys.map((key) => key.chainName!);
}
static fromSerializedAddressFile(
multiProvider: MultiProvider<any>,
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,
),
)
.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,
});
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<any>,
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();
@ -151,22 +216,28 @@ async function main() {
),
desiredRelayerBalance: desiredBalance,
},
context: this.context,
});
for (const relayerKey of relayerKeys.filter(
(key) => key.chainName !== chain,
)) {
await relayerKey.fetch();
for (const key of this.keys.filter((k) => k.chainName !== chain)) {
await key.fetch();
try {
await fundRelayer(chainConnection, relayerKey, desiredBalance);
await this.fundRelayerIfRequired(
chainConnection,
chain,
key,
desiredBalance,
);
} catch (err) {
error('Error funding relayer', {
relayer: relayerKeyInfo(relayerKey),
relayer: relayerKeyInfo(key),
context: this.context,
error: err,
});
failureOccurred = true;
}
}
walletBalanceGauge
.labels({
chain,
@ -178,41 +249,72 @@ async function main() {
})
.set(
parseFloat(
ethers.utils.formatEther(await chainConnection.signer!.getBalance()),
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,
const relayerInfo = relayerKeyInfo(key);
if (delta.gt(minDelta)) {
log('Sending relayer funds...', {
relayer: relayerInfo,
amount: ethers.utils.formatEther(delta),
context: this.context,
chain,
});
// 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 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,
};
}

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

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

@ -16,6 +16,9 @@ export function runRelayerFunderHelmCommand<Chain extends ChainName>(
`helm ${helmCommand} relayer-funder ./helm/relayer-funder --namespace ${
relayerFunderConfig.namespace
} ${values.join(' ')}`,
{},
false,
true,
);
}
@ -29,7 +32,10 @@ function getRelayerFunderHelmValues<Chain extends ChainName>(
},
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,

Loading…
Cancel
Save