diff --git a/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml b/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml index f0870da0a..115e249cd 100644 --- a/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml +++ b/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml @@ -33,7 +33,6 @@ spec: */}} {{- range .Values.hyperlane.chains }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} - GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }} {{- end }} {{- if .Values.hyperlane.aws }} AWS_ACCESS_KEY_ID: {{ print "'{{ .aws_access_key_id | toString }}'" }} @@ -51,9 +50,6 @@ spec: - secretKey: {{ printf "%s_rpcs" . }} remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }} - - secretKey: {{ printf "%s_rpc" . }} - remoteRef: - key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }} {{- end }} {{- if .Values.hyperlane.aws }} - secretKey: aws_access_key_id diff --git a/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml b/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml index de6577bfa..2a95d627e 100644 --- a/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml +++ b/typescript/infra/helm/key-funder/templates/env-var-external-secret.yaml @@ -29,7 +29,6 @@ spec: */}} {{- range .Values.hyperlane.chains }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} - GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }} {{- end }} data: - secretKey: deployer_key @@ -43,7 +42,4 @@ spec: - secretKey: {{ printf "%s_rpcs" . }} remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }} - - secretKey: {{ printf "%s_rpc" . }} - remoteRef: - key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }} {{- end }} diff --git a/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml b/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml index a8d44b48c..62b311712 100644 --- a/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml +++ b/typescript/infra/helm/liquidity-layer-relayers/templates/env-var-external-secret.yaml @@ -29,7 +29,6 @@ spec: */}} {{- range .Values.hyperlane.chains }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} - GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINT_{{ . | upper }}: {{ printf "'{{ .%s_rpc | toString }}'" . }} {{- end }} data: - secretKey: deployer_key @@ -43,7 +42,4 @@ spec: - secretKey: {{ printf "%s_rpcs" . }} remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv . }} - - secretKey: {{ printf "%s_rpc" . }} - remoteRef: - key: {{ printf "%s-rpc-endpoint-%s" $.Values.hyperlane.runEnv . }} {{- end }} diff --git a/typescript/infra/scripts/agent-utils.ts b/typescript/infra/scripts/agent-utils.ts index 5f814e827..313af5d44 100644 --- a/typescript/infra/scripts/agent-utils.ts +++ b/typescript/infra/scripts/agent-utils.ts @@ -101,13 +101,6 @@ export function withModuleAndFork(args: Argv) { .alias('f', 'fork'); } -export function withNetwork(args: Argv) { - return args - .describe('network', 'network to target') - .choices('network', getChains()) - .alias('n', 'network'); -} - export function withContext(args: Argv) { return args .describe('context', 'deploy context') @@ -117,6 +110,17 @@ export function withContext(args: Argv) { .demandOption('context'); } +export function withChainRequired(args: Argv) { + return withChain(args).demandOption('chain'); +} + +export function withChain(args: Argv) { + return args + .describe('chain', 'chain name') + .choices('chain', getChains()) + .alias('c', 'chain'); +} + export function withProtocol(args: Argv) { return args .describe('protocol', 'protocol type') @@ -176,6 +180,17 @@ export function withConcurrentDeploy(args: Argv) { .default('concurrentDeploy', false); } +export function withRpcUrls(args: Argv) { + return args + .describe( + 'rpcUrls', + 'rpc urls in a comma separated list, in order of preference', + ) + .string('rpcUrls') + .demandOption('rpcUrls') + .alias('r', 'rpcUrls'); +} + // not requiring to build coreConfig to get agentConfig export async function getAgentConfigsBasedOnArgs(argv?: { environment: DeployEnvironment; diff --git a/typescript/infra/scripts/check-rpc-urls.ts b/typescript/infra/scripts/check-rpc-urls.ts index 95ba7ee4c..307e4515c 100644 --- a/typescript/infra/scripts/check-rpc-urls.ts +++ b/typescript/infra/scripts/check-rpc-urls.ts @@ -15,10 +15,7 @@ async function main() { const providers: [string, ethers.providers.JsonRpcProvider][] = []; for (const chain of chains) { rootLogger.debug(`Building providers for ${chain}`); - const rpcData = [ - ...(await getSecretRpcEndpoints(environment, chain, false)), - ...(await getSecretRpcEndpoints(environment, chain, true)), - ]; + const rpcData = await getSecretRpcEndpoints(environment, chain); for (const url of rpcData) providers.push([chain, new ethers.providers.StaticJsonRpcProvider(url)]); } diff --git a/typescript/infra/scripts/deploy.ts b/typescript/infra/scripts/deploy.ts index 733bfd819..fc6719051 100644 --- a/typescript/infra/scripts/deploy.ts +++ b/typescript/infra/scripts/deploy.ts @@ -42,10 +42,10 @@ import { getArgs, getModuleDirectory, withBuildArtifactPath, + withChain, withConcurrentDeploy, withContext, withModuleAndFork, - withNetwork, } from './agent-utils.js'; import { getEnvironmentConfig, getHyperlaneCore } from './core-utils.js'; @@ -55,12 +55,12 @@ async function main() { module, fork, environment, - network, + chain, buildArtifactPath, concurrentDeploy, } = await withContext( withConcurrentDeploy( - withNetwork(withModuleAndFork(withBuildArtifactPath(getArgs()))), + withChain(withModuleAndFork(withBuildArtifactPath(getArgs()))), ), ).argv; const envConfig = getEnvironmentConfig(environment); @@ -233,7 +233,7 @@ async function main() { // prompt for confirmation in production environments if (environment !== 'test' && !fork) { - const confirmConfig = network ? config[network] : config; + const confirmConfig = chain ? config[chain] : config; console.log(JSON.stringify(confirmConfig, null, 2)); const { value: confirmed } = await prompts({ type: 'confirm', @@ -250,7 +250,7 @@ async function main() { config, deployer, cache, - network ?? fork, + chain ?? fork, agentConfig, ); } diff --git a/typescript/infra/scripts/secret-rpc-urls/get-rpc-urls.ts b/typescript/infra/scripts/secret-rpc-urls/get-rpc-urls.ts new file mode 100644 index 000000000..0f2c9fd5f --- /dev/null +++ b/typescript/infra/scripts/secret-rpc-urls/get-rpc-urls.ts @@ -0,0 +1,26 @@ +import { + getSecretRpcEndpoints, + secretRpcEndpointsExist, +} from '../../src/agents/index.js'; +import { getArgs, withChainRequired } from '../agent-utils.js'; + +async function main() { + const { environment, chain } = await withChainRequired(getArgs()).argv; + const secretExists = await secretRpcEndpointsExist(environment, chain); + if (!secretExists) { + console.log( + `No secret rpc urls found for ${chain} in ${environment} environment`, + ); + process.exit(0); + } + + const secrets = await getSecretRpcEndpoints(environment, chain); + console.log(secrets); +} + +main() + .then() + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/typescript/infra/scripts/secret-rpc-urls/set-rpc-urls.ts b/typescript/infra/scripts/secret-rpc-urls/set-rpc-urls.ts new file mode 100644 index 000000000..78881a01e --- /dev/null +++ b/typescript/infra/scripts/secret-rpc-urls/set-rpc-urls.ts @@ -0,0 +1,120 @@ +import { confirm } from '@inquirer/prompts'; +import { ethers } from 'ethers'; + +import { + getSecretRpcEndpoints, + getSecretRpcEndpointsLatestVersionName, + secretRpcEndpointsExist, + setSecretRpcEndpoints, +} from '../../src/agents/index.js'; +import { disableGCPSecretVersion } from '../../src/utils/gcloud.js'; +import { isEthereumProtocolChain } from '../../src/utils/utils.js'; +import { getArgs, withChainRequired, withRpcUrls } from '../agent-utils.js'; + +async function testProviders(rpcUrlsArray: string[]): Promise { + let providersSucceeded = true; + for (const url of rpcUrlsArray) { + const provider = new ethers.providers.StaticJsonRpcProvider(url); + try { + const blockNumber = await provider.getBlockNumber(); + console.log(`Valid provider for ${url} with block number ${blockNumber}`); + } catch (e) { + console.error(`Provider failed: ${url}`); + providersSucceeded = false; + } + } + + return providersSucceeded; +} + +async function main() { + const { environment, chain, rpcUrls } = await withRpcUrls( + withChainRequired(getArgs()), + ).argv; + + const rpcUrlsArray = rpcUrls + .split(/,\s*/) + .filter(Boolean) // filter out empty strings + .map((url) => url.trim()); + + if (!rpcUrlsArray.length) { + console.error('No rpc urls provided, Exiting.'); + process.exit(1); + } + + const secretPayload = JSON.stringify(rpcUrlsArray); + + const secretExists = await secretRpcEndpointsExist(environment, chain); + if (!secretExists) { + console.log( + `No secret rpc urls found for ${chain} in ${environment} environment\n`, + ); + } else { + const currentSecrets = await getSecretRpcEndpoints(environment, chain); + console.log( + `Current secrets found for ${chain} in ${environment} environment:\n${JSON.stringify( + currentSecrets, + null, + 2, + )}\n`, + ); + } + + const confirmedSet = await confirm({ + message: `Are you sure you want to set the following RPC URLs for ${chain} in ${environment}?\n${secretPayload}\n`, + }); + + if (!confirmedSet) { + console.log('Exiting without setting secret.'); + process.exit(0); + } + + if (isEthereumProtocolChain(chain)) { + console.log('\nTesting providers...'); + const testPassed = await testProviders(rpcUrlsArray); + if (!testPassed) { + console.error('At least one provider failed. Exiting.'); + process.exit(1); + } + + const confirmedProviders = await confirm({ + message: `All providers passed. Do you want to continue setting the secret?\n`, + }); + + if (!confirmedProviders) { + console.log('Exiting without setting secret.'); + process.exit(0); + } + } else { + console.log( + 'Skipping provider testing as chain is not an Ethereum protocol chain.', + ); + } + + let latestVersionName; + if (secretExists) { + latestVersionName = await getSecretRpcEndpointsLatestVersionName( + environment, + chain, + ); + } + console.log(`Setting secret...`); + await setSecretRpcEndpoints(environment, chain, secretPayload); + console.log(`Added secret version!`); + + if (latestVersionName) { + try { + await disableGCPSecretVersion(latestVersionName); + console.log(`Disabled previous version of the secret!`); + } catch (e) { + console.log(`Could not disable previous version of the secret`); + } + } +} + +main() + .then() + .catch((e) => { + console.error(e); + process.exit(1); + }); diff --git a/typescript/infra/scripts/verify.ts b/typescript/infra/scripts/verify.ts index 9c15c6597..0e1f782d4 100644 --- a/typescript/infra/scripts/verify.ts +++ b/typescript/infra/scripts/verify.ts @@ -12,12 +12,12 @@ import { } from '../src/deployment/verify.js'; import { readJSONAtPath } from '../src/utils/utils.js'; -import { getArgs, withBuildArtifactPath, withNetwork } from './agent-utils.js'; +import { getArgs, withBuildArtifactPath, withChain } from './agent-utils.js'; import { getEnvironmentConfig } from './core-utils.js'; async function main() { - const { environment, buildArtifactPath, verificationArtifactPath, network } = - await withNetwork(withBuildArtifactPath(getArgs())) + const { environment, buildArtifactPath, verificationArtifactPath, chain } = + await withChain(withBuildArtifactPath(getArgs())) .string('verificationArtifactPath') .describe( 'verificationArtifactPath', @@ -54,7 +54,7 @@ async function main() { // verify all the things const failedResults = ( - await verifier.verify(network ? [network] : undefined) + await verifier.verify(chain ? [chain] : undefined) ).filter((result) => result.status === 'rejected'); // only log the failed verifications to console diff --git a/typescript/infra/src/agents/index.ts b/typescript/infra/src/agents/index.ts index 66a9be195..c959bca65 100644 --- a/typescript/infra/src/agents/index.ts +++ b/typescript/infra/src/agents/index.ts @@ -17,7 +17,12 @@ import { ScraperConfigHelper } from '../config/agent/scraper.js'; import { ValidatorConfigHelper } from '../config/agent/validator.js'; import { DeployEnvironment } from '../config/environment.js'; import { AgentRole, Role } from '../roles.js'; -import { fetchGCPSecret } from '../utils/gcloud.js'; +import { + fetchGCPSecret, + gcpSecretExistsUsingClient, + getGcpSecretLatestVersionName, + setGCPSecretUsingClient, +} from '../utils/gcloud.js'; import { HelmCommand, buildHelmChartDependencies, @@ -287,6 +292,13 @@ export class ValidatorHelmManager extends MultichainAgentHelmManager { } } +export function getSecretName( + environment: string, + chainName: ChainName, +): string { + return `${environment}-rpc-endpoints-${chainName}`; +} + export async function getSecretAwsCredentials(agentConfig: AgentContextConfig) { return { accessKeyId: await fetchGCPSecret( @@ -303,17 +315,11 @@ export async function getSecretAwsCredentials(agentConfig: AgentContextConfig) { export async function getSecretRpcEndpoints( environment: string, chainName: ChainName, - multipleEndpoints = false, ): Promise { - const secret = await fetchGCPSecret( - `${environment}-rpc-endpoint${multipleEndpoints ? 's' : ''}-${chainName}`, - multipleEndpoints, - ); - if (typeof secret != 'string' && !Array.isArray(secret)) { - throw Error(`Expected secret for ${chainName} rpc endpoint`); - } + const secret = await fetchGCPSecret(getSecretName(environment, chainName)); + if (!Array.isArray(secret)) { - return [secret.trimEnd()]; + throw Error(`Expected secret for ${chainName} rpc endpoint`); } return secret.map((i) => { @@ -323,6 +329,29 @@ export async function getSecretRpcEndpoints( }); } +export async function getSecretRpcEndpointsLatestVersionName( + environment: string, + chainName: ChainName, +) { + return getGcpSecretLatestVersionName(getSecretName(environment, chainName)); +} + +export async function secretRpcEndpointsExist( + environment: string, + chainName: ChainName, +): Promise { + return gcpSecretExistsUsingClient(getSecretName(environment, chainName)); +} + +export async function setSecretRpcEndpoints( + environment: string, + chainName: ChainName, + endpoints: string, +) { + const secretName = getSecretName(environment, chainName); + await setGCPSecretUsingClient(secretName, endpoints); +} + export async function getSecretDeployerKey( environment: DeployEnvironment, context: Contexts, diff --git a/typescript/infra/src/config/chain.ts b/typescript/infra/src/config/chain.ts index 2be5bc4ea..51584fc37 100644 --- a/typescript/infra/src/config/chain.ts +++ b/typescript/infra/src/config/chain.ts @@ -106,7 +106,7 @@ export async function getSecretMetadataOverrides( const secretRpcUrls = await Promise.all( chains.map(async (chain) => { - const rpcUrls = await getSecretRpcEndpoints(deployEnv, chain, true); + const rpcUrls = await getSecretRpcEndpoints(deployEnv, chain); return { chain, rpcUrls, diff --git a/typescript/infra/src/utils/gcloud.ts b/typescript/infra/src/utils/gcloud.ts index cc4fd2d62..56d913b7c 100644 --- a/typescript/infra/src/utils/gcloud.ts +++ b/typescript/infra/src/utils/gcloud.ts @@ -45,9 +45,7 @@ export async function fetchGCPSecret( } export async function fetchLatestGCPSecret(secretName: string) { - const client = new SecretManagerServiceClient({ - projectId: GCP_PROJECT_ID, - }); + const client = await getSecretManagerServiceClient(); const [secretVersion] = await client.accessSecretVersion({ name: `projects/${GCP_PROJECT_ID}/secrets/${secretName}/versions/latest`, }); @@ -84,6 +82,12 @@ function tryGCPSecretFromEnvVariable(gcpSecretName: string) { return process.env[overrideEnvVarName]; } +/** + * Checks if a secret exists in GCP using the gcloud CLI. + * @deprecated Use gcpSecretExistsUsingClient instead. + * @param secretName The name of the secret to check. + * @returns A boolean indicating whether the secret exists. + */ export async function gcpSecretExists(secretName: string) { const fullName = `projects/${await getCurrentProjectNumber()}/secrets/${secretName}`; debugLog(`Checking if GCP secret exists for ${fullName}`); @@ -95,6 +99,55 @@ export async function gcpSecretExists(secretName: string) { return matches.length > 0; } +/** + * Uses the SecretManagerServiceClient to check if a secret exists. + * @param secretName The name of the secret to check. + * @returns A boolean indicating whether the secret exists. + */ +export async function gcpSecretExistsUsingClient( + secretName: string, + client?: SecretManagerServiceClient, +): Promise { + if (!client) { + client = await getSecretManagerServiceClient(); + } + + try { + const fullSecretName = `projects/${await getCurrentProjectNumber()}/secrets/${secretName}`; + const [secrets] = await client.listSecrets({ + parent: `projects/${GCP_PROJECT_ID}`, + filter: `name=${fullSecretName}`, + }); + + return secrets.length > 0; + } catch (e) { + debugLog(`Error checking if secret exists: ${e}`); + throw e; + } +} + +export async function getGcpSecretLatestVersionName(secretName: string) { + const client = await getSecretManagerServiceClient(); + const [version] = await client.getSecretVersion({ + name: `projects/${GCP_PROJECT_ID}/secrets/${secretName}/versions/latest`, + }); + + return version?.name; +} + +export async function getSecretManagerServiceClient() { + return new SecretManagerServiceClient({ + projectId: GCP_PROJECT_ID, + }); +} + +/** + * Sets a GCP secret using the gcloud CLI. Create secret if it doesn't exist and add a new version or update the existing one. + * @deprecated Use setGCPSecretUsingClient instead. + * @param secretName The name of the secret to set. + * @param secret The secret to set. + * @param labels The labels to set on the secret. + */ export async function setGCPSecret( secretName: string, secret: string, @@ -121,6 +174,64 @@ export async function setGCPSecret( await rm(fileName); } +/** + * Sets a GCP secret using the SecretManagerServiceClient. Create secret if it doesn't exist and add a new version or update the existing one. + * @param secretName The name of the secret to set. + * @param secret The secret to set. + */ +export async function setGCPSecretUsingClient( + secretName: string, + secret: string, + labels?: Record, +) { + const client = await getSecretManagerServiceClient(); + + const exists = await gcpSecretExistsUsingClient(secretName, client); + if (!exists) { + // Create the secret + await client.createSecret({ + parent: `projects/${GCP_PROJECT_ID}`, + secretId: secretName, + secret: { + name: secretName, + replication: { + automatic: {}, + }, + labels, + }, + }); + debugLog(`Created new GCP secret for ${secretName}`); + } + await addGCPSecretVersion(secretName, secret, client); +} + +export async function addGCPSecretVersion( + secretName: string, + secret: string, + client?: SecretManagerServiceClient, +) { + if (!client) { + client = await getSecretManagerServiceClient(); + } + + const [version] = await client.addSecretVersion({ + parent: `projects/${GCP_PROJECT_ID}/secrets/${secretName}`, + payload: { + data: Buffer.from(secret, 'utf8'), + }, + }); + debugLog(`Added secret version ${version?.name}`); +} + +export async function disableGCPSecretVersion(secretName: string) { + const client = await getSecretManagerServiceClient(); + + const [version] = await client.disableSecretVersion({ + name: secretName, + }); + debugLog(`Disabled secret version ${version?.name}`); +} + // Returns the email of the service account export async function createServiceAccountIfNotExists( serviceAccountName: string,