From 166e9d316aa24a9682e828ce7da1e526c8b07570 Mon Sep 17 00:00:00 2001 From: Paul Balaji Date: Thu, 1 Feb 2024 16:58:12 +0000 Subject: [PATCH 1/9] feat: improve create-key logging (#3207) --- .../infra/scripts/announce-validators.ts | 8 ++- typescript/infra/scripts/utils.ts | 19 +++++-- typescript/infra/src/agents/aws/key.ts | 50 +++++++++++++++++-- typescript/infra/src/agents/gcp.ts | 32 +++++++++--- typescript/infra/src/agents/key-utils.ts | 47 ++++++++++++----- typescript/infra/src/utils/gcloud.ts | 25 ++++++++-- 6 files changed, 150 insertions(+), 31 deletions(-) diff --git a/typescript/infra/scripts/announce-validators.ts b/typescript/infra/scripts/announce-validators.ts index 509c0d5237..f070329f3e 100644 --- a/typescript/infra/scripts/announce-validators.ts +++ b/typescript/infra/scripts/announce-validators.ts @@ -126,7 +126,9 @@ async function main() { const announced = announcedLocations[0].includes(location); if (!announced) { const signature = ethers.utils.joinSignature(announcement.signature); - console.log(`Announcing ${address} checkpoints at ${location}`); + console.log( + `[${chain}] Announcing ${address} checkpoints at ${location}`, + ); await validatorAnnounce.announce( address, location, @@ -134,7 +136,9 @@ async function main() { multiProvider.getTransactionOverrides(chain), ); } else { - console.log(`Already announced ${address} checkpoints at ${location}`); + console.log( + `[${chain}] Already announced ${address} checkpoints at ${location}`, + ); } } } diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index 645aa2ca05..9447f4d24a 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import path from 'path'; import yargs from 'yargs'; @@ -29,6 +30,8 @@ import { EnvironmentNames, deployEnvToSdkEnv } from '../src/config/environment'; import { Role } from '../src/roles'; import { assertContext, assertRole, readJSON } from '../src/utils/utils'; +const debugLog = debug('infra:scripts:utils'); + export enum Modules { // TODO: change PROXY_FACTORY = 'ism', @@ -130,6 +133,7 @@ export function assertEnvironment(env: string): DeployEnvironment { } export function getEnvironmentConfig(environment: DeployEnvironment) { + debugLog(`Getting environment config for ${environment}`); return environments[environment]; } @@ -154,12 +158,17 @@ export function getAgentConfig( typeof environment == 'string' ? getEnvironmentConfig(environment) : environment; + + debugLog( + `Getting agent config for ${context} context in ${coreConfig.environment} environment`, + ); + const agentConfig = coreConfig.agents[context]; if (!agentConfig) throw Error( - `Invalid context ${context} for environment, must be one of ${Object.keys( - coreConfig.agents, - )}.`, + `Invalid context ${context} for ${ + coreConfig.environment + } environment, must be one of ${Object.keys(coreConfig.agents)}.`, ); return agentConfig; } @@ -171,6 +180,7 @@ export function getKeyForRole( role: Role, index?: number, ): CloudAgentKey { + debugLog(`Getting key for ${role} role`); const environmentConfig = environments[environment]; const agentConfig = getAgentConfig(context, environmentConfig); return getCloudAgentKey(agentConfig, role, chain, index); @@ -185,7 +195,9 @@ export async function getMultiProviderForRole( // TODO: rename to consensusType? connectionType?: RpcConsensusType, ): Promise { + debugLog(`Getting multiprovider for ${role} role`); if (process.env.CI === 'true') { + debugLog('Returning multiprovider with default RPCs in CI'); return new MultiProvider(); // use default RPCs } const multiProvider = new MultiProvider(txConfigs); @@ -212,6 +224,7 @@ export async function getKeysForRole( index?: number, ): Promise> { if (process.env.CI === 'true') { + debugLog('No keys to return in CI'); return {}; } diff --git a/typescript/infra/src/agents/aws/key.ts b/typescript/infra/src/agents/aws/key.ts index fb42d10aab..a6c47369e5 100644 --- a/typescript/infra/src/agents/aws/key.ts +++ b/typescript/infra/src/agents/aws/key.ts @@ -16,6 +16,7 @@ import { UpdateAliasCommand, } from '@aws-sdk/client-kms'; import { KmsEthersSigner } from 'aws-kms-ethers-signer'; +import { Debugger, debug } from 'debug'; import { ethers } from 'ethers'; import { AgentSignerKeyType, ChainName } from '@hyperlane-xyz/sdk'; @@ -41,6 +42,7 @@ export class AgentAwsKey extends CloudAgentKey { private client: KMSClient | undefined; private region: string; public remoteKey: RemoteKey = { fetched: false }; + protected logger: Debugger; constructor( agentConfig: AgentContextConfig, @@ -53,16 +55,22 @@ export class AgentAwsKey extends CloudAgentKey { throw new Error('Not configured as AWS'); } this.region = agentConfig.aws.region; + this.logger = debug(`infra:agents:key:aws:${this.identifier}`); } get privateKey(): string { + this.logger( + 'Attempting to access private key, which is unavailable for AWS keys', + ); throw new Error('Private key unavailable for AWS keys'); } async getClient(): Promise { if (this.client) { + this.logger('Returning existing KMSClient instance'); return this.client; } + this.logger('Creating new KMSClient instance'); this.client = new KMSClient({ region: this.region, }); @@ -94,6 +102,7 @@ export class AgentAwsKey extends CloudAgentKey { } async fetch() { + this.logger('Fetching key'); const address = await this.fetchAddressFromAws(); this.remoteKey = { fetched: true, @@ -102,24 +111,28 @@ export class AgentAwsKey extends CloudAgentKey { } async createIfNotExists() { + this.logger('Checking if key exists and creating if not'); const keyId = await this.getId(); // If it doesn't exist, create it if (!keyId) { - // TODO should this be awaited? create is async - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.create(); + this.logger('Key does not exist, creating new key'); + await this.create(); // It can take a moment for the change to propagate await sleep(1000); + } else { + this.logger('Key already exists'); } await this.fetch(); } async delete() { + this.logger('Delete operation called, but not implemented'); throw Error('Not implemented yet'); } // Allows the `userArn` to use the key async putKeyPolicy(userArn: string) { + this.logger(`Putting key policy for user ARN: ${userArn}`); const client = await this.getClient(); const policy = { Version: '2012-10-17', @@ -151,22 +164,29 @@ export class AgentAwsKey extends CloudAgentKey { PolicyName: 'default', // This is the only accepted name }); await client.send(cmd); + this.logger('Key policy put successfully'); } // Gets the Key's ID if it exists, undefined otherwise async getId() { try { + this.logger('Attempting to describe key to get ID'); const keyDescription = await this.describeKey(); - return keyDescription.KeyMetadata?.KeyId; + const keyId = keyDescription.KeyMetadata?.KeyId; + this.logger(`Key ID retrieved: ${keyId}`); + return keyId; } catch (err: any) { if (err.name === 'NotFoundException') { + this.logger('Key not found'); return undefined; } + this.logger(`Error retrieving key ID: ${err}`); throw err; } } create() { + this.logger('Creating new key'); return this._create(false); } @@ -175,6 +195,7 @@ export class AgentAwsKey extends CloudAgentKey { * @returns The address of the new key */ update() { + this.logger('Updating key (creating new key for rotation)'); return this._create(true); } @@ -182,6 +203,7 @@ export class AgentAwsKey extends CloudAgentKey { * Requires update to have been called on this key prior */ async rotate() { + this.logger('Rotating keys'); const canonicalAlias = this.identifier; const newAlias = canonicalAlias + '-new'; const oldAlias = canonicalAlias + '-old'; @@ -226,15 +248,19 @@ export class AgentAwsKey extends CloudAgentKey { // Address should have changed now await this.fetch(); + this.logger('Keys rotated successfully'); } async getSigner( provider?: ethers.providers.Provider, ): Promise { + this.logger('Getting signer'); const keyId = await this.getId(); if (!keyId) { + this.logger('Key ID not defined, cannot get signer'); throw Error('Key ID not defined'); } + this.logger(`Creating KmsEthersSigner with key ID: ${keyId}`); // @ts-ignore We're using a newer version of Provider than // KmsEthersSigner. The return type for getFeeData for this newer // type is a superset of the return type for getFeeData for the older type, @@ -252,12 +278,15 @@ export class AgentAwsKey extends CloudAgentKey { private requireFetched() { if (!this.remoteKey.fetched) { + this.logger('Key has not been fetched yet'); throw new Error('Key not fetched'); } + this.logger('Key has been fetched'); } // Creates a new key and returns its address private async _create(rotate: boolean) { + this.logger(`Creating key with rotation: ${rotate}`); const client = await this.getClient(); const alias = this.identifier; if (!rotate) { @@ -269,6 +298,7 @@ export class AgentAwsKey extends CloudAgentKey { (_) => _.AliasName === alias, ); if (match) { + this.logger(`Alias ${alias} already exists`); throw new Error( `Attempted to create new key but alias ${alias} already exists`, ); @@ -288,6 +318,7 @@ export class AgentAwsKey extends CloudAgentKey { const createResponse = await client.send(command); if (!createResponse.KeyMetadata) { + this.logger('KeyMetadata was not returned when creating the key'); throw new Error('KeyMetadata was not returned when creating the key'); } const keyId = createResponse.KeyMetadata?.KeyId; @@ -298,10 +329,12 @@ export class AgentAwsKey extends CloudAgentKey { ); const address = this.fetchAddressFromAws(keyId); + this.logger(`New key created with ID: ${keyId}`); return address; } private async fetchAddressFromAws(keyId?: string) { + this.logger(`Fetching address from AWS for key ID: ${keyId}`); const client = await this.getClient(); if (!keyId) { @@ -312,10 +345,15 @@ export class AgentAwsKey extends CloudAgentKey { new GetPublicKeyCommand({ KeyId: keyId }), ); - return getEthereumAddress(Buffer.from(publicKeyResponse.PublicKey!)); + const address = getEthereumAddress( + Buffer.from(publicKeyResponse.PublicKey!), + ); + this.logger(`Address fetched: ${address}`); + return address; } private async describeKey(): Promise { + this.logger('Describing key'); const client = await this.getClient(); return client.send( new DescribeKeyCommand({ @@ -325,6 +363,7 @@ export class AgentAwsKey extends CloudAgentKey { } private async getAliases(): Promise { + this.logger('Getting aliases'); const client = await this.getClient(); let aliases: AliasListEntry[] = []; let marker: string | undefined = undefined; @@ -350,6 +389,7 @@ export class AgentAwsKey extends CloudAgentKey { break; } } + this.logger(`Aliases retrieved: ${aliases.length}`); return aliases; } } diff --git a/typescript/infra/src/agents/gcp.ts b/typescript/infra/src/agents/gcp.ts index 4ba314256e..e0fc75874a 100644 --- a/typescript/infra/src/agents/gcp.ts +++ b/typescript/infra/src/agents/gcp.ts @@ -1,9 +1,6 @@ -import { - encodeSecp256k1Pubkey, - pubkeyToAddress, - rawSecp256k1PubkeyToRawAddress, -} from '@cosmjs/amino'; +import { encodeSecp256k1Pubkey, pubkeyToAddress } from '@cosmjs/amino'; import { Keypair } from '@solana/web3.js'; +import { Debugger, debug } from 'debug'; import { Wallet, ethers } from 'ethers'; import { ChainName } from '@hyperlane-xyz/sdk'; @@ -42,6 +39,8 @@ interface FetchedKey { type RemoteKey = UnfetchedKey | FetchedKey; export class AgentGCPKey extends CloudAgentKey { + protected logger: Debugger; + constructor( environment: DeployEnvironment, context: Contexts, @@ -51,18 +50,23 @@ export class AgentGCPKey extends CloudAgentKey { private remoteKey: RemoteKey = { fetched: false }, ) { super(environment, context, role, chainName, index); + this.logger = debug(`infra:agents:key:gcp:${this.identifier}`); } async createIfNotExists() { + this.logger('Checking if key exists and creating if not'); try { await this.fetch(); + this.logger('Key already exists'); } catch (err) { + this.logger('Key does not exist, creating new key'); await this.create(); } } serializeAsAddress() { this.requireFetched(); + this.logger('Serializing key as address'); return { identifier: this.identifier, // @ts-ignore @@ -98,6 +102,7 @@ export class AgentGCPKey extends CloudAgentKey { addressForProtocol(protocol: ProtocolType): string | undefined { this.requireFetched(); + this.logger(`Getting address for protocol: ${protocol}`); switch (protocol) { case ProtocolType.Ethereum: @@ -106,7 +111,7 @@ export class AgentGCPKey extends CloudAgentKey { return Keypair.fromSeed( Buffer.from(strip0x(this.privateKey), 'hex'), ).publicKey.toBase58(); - case ProtocolType.Cosmos: + case ProtocolType.Cosmos: { const compressedPubkey = ethers.utils.computePublicKey( this.privateKey, true, @@ -117,12 +122,15 @@ export class AgentGCPKey extends CloudAgentKey { // TODO support other prefixes? // https://cosmosdrops.io/en/tools/bech32-converter is useful for converting to other prefixes. return pubkeyToAddress(encodedPubkey, 'neutron'); + } default: + this.logger(`Unsupported protocol: ${protocol}`); return undefined; } } async fetch() { + this.logger('Fetching key'); const secret: SecretManagerPersistedKeys = (await fetchGCPSecret( this.identifier, )) as any; @@ -131,25 +139,34 @@ export class AgentGCPKey extends CloudAgentKey { privateKey: secret.privateKey, address: secret.address, }; + this.logger(`Key fetched successfully: ${secret.address}`); } async create() { + this.logger('Creating new key'); this.remoteKey = await this._create(false); + this.logger('Key created successfully'); } async update() { + this.logger('Updating key'); this.remoteKey = await this._create(true); + this.logger('Key updated successfully'); return this.address; } async delete() { + this.logger('Deleting key'); await execCmd(`gcloud secrets delete ${this.identifier} --quiet`); + this.logger('Key deleted successfully'); } async getSigner( provider?: ethers.providers.Provider, ): Promise { + this.logger('Getting signer'); if (!this.remoteKey.fetched) { + this.logger('Key not fetched, fetching now'); await this.fetch(); } return new Wallet(this.privateKey, provider); @@ -157,12 +174,14 @@ export class AgentGCPKey extends CloudAgentKey { private requireFetched() { if (!this.remoteKey.fetched) { + this.logger('Key not fetched, throwing error'); throw new Error("Can't persist without address"); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars private async _create(rotate: boolean) { + this.logger(`Creating key with rotation: ${rotate}`); const wallet = Wallet.createRandom(); const address = await wallet.getAddress(); const identifier = this.identifier; @@ -187,6 +206,7 @@ export class AgentGCPKey extends CloudAgentKey { }), }, ); + this.logger('Key creation data persisted to GCP'); return { fetched: true, diff --git a/typescript/infra/src/agents/key-utils.ts b/typescript/infra/src/agents/key-utils.ts index 11e669d238..b597cb395e 100644 --- a/typescript/infra/src/agents/key-utils.ts +++ b/typescript/infra/src/agents/key-utils.ts @@ -1,3 +1,5 @@ +import debug from 'debug'; + import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; import { objMap } from '@hyperlane-xyz/utils'; @@ -17,6 +19,8 @@ import { AgentAwsKey } from './aws/key'; import { AgentGCPKey } from './gcp'; import { CloudAgentKey } from './keys'; +const debugLog = debug('infra:agents:key:utils'); + export interface KeyAsAddress { identifier: string; address: string; @@ -156,6 +160,7 @@ function getRoleKeyMapPerChain( export function getAllCloudAgentKeys( agentConfig: RootAgentConfig, ): Array { + debugLog('Retrieving all cloud agent keys'); const keysPerChain = getRoleKeyMapPerChain(agentConfig); const keysByIdentifier = Object.keys(keysPerChain).reduce( @@ -190,21 +195,22 @@ export function getCloudAgentKey( chainName?: ChainName, index?: number, ): CloudAgentKey { + debugLog(`Retrieving cloud agent key for ${role} on ${chainName}`); switch (role) { case Role.Validator: if (chainName === undefined || index === undefined) { - throw Error(`Must provide chainName and index for validator key`); + throw Error('Must provide chainName and index for validator key'); } // For now just get the validator key, and not the chain signer. return getValidatorKeysForChain(agentConfig, chainName, index).validator; case Role.Relayer: if (chainName === undefined) { - throw Error(`Must provide chainName for relayer key`); + throw Error('Must provide chainName for relayer key'); } return getRelayerKeyForChain(agentConfig, chainName); case Role.Kathy: if (chainName === undefined) { - throw Error(`Must provide chainName for kathy key`); + throw Error('Must provide chainName for kathy key'); } return getKathyKeyForChain(agentConfig, chainName); case Role.Deployer: @@ -223,6 +229,7 @@ export function getRelayerKeyForChain( agentConfig: AgentContextConfig, chainName: ChainName, ): CloudAgentKey { + debugLog(`Retrieving relayer key for ${chainName}`); // If AWS is enabled and the chain is an Ethereum-based chain, we want to use // an AWS key. if (agentConfig.aws && isEthereumProtocolChain(chainName)) { @@ -240,6 +247,7 @@ export function getKathyKeyForChain( agentConfig: AgentContextConfig, chainName: ChainName, ): CloudAgentKey { + debugLog(`Retrieving kathy key for ${chainName}`); // If AWS is enabled and the chain is an Ethereum-based chain, we want to use // an AWS key. if (agentConfig.aws && isEthereumProtocolChain(chainName)) { @@ -252,6 +260,7 @@ export function getKathyKeyForChain( // Returns the deployer key. This is always a GCP key, not chain specific, // and in the Hyperlane context. export function getDeployerKey(agentConfig: AgentContextConfig): CloudAgentKey { + debugLog('Retrieving deployer key'); return new AgentGCPKey(agentConfig.runEnv, Contexts.Hyperlane, Role.Deployer); } @@ -267,6 +276,7 @@ export function getValidatorKeysForChain( validator: CloudAgentKey; chainSigner: CloudAgentKey; } { + debugLog(`Retrieving validator keys for ${chainName}`); const validator = agentConfig.aws ? new AgentAwsKey(agentConfig, Role.Validator, chainName, index) : new AgentGCPKey( @@ -279,15 +289,19 @@ export function getValidatorKeysForChain( // If the chain is Ethereum-based, we can just use the validator key (even if it's AWS-based) // as the chain signer. Otherwise, we need to use a GCP key. - const chainSigner = isEthereumProtocolChain(chainName) - ? validator - : new AgentGCPKey( - agentConfig.runEnv, - agentConfig.context, - Role.Validator, - chainName, - index, - ); + let chainSigner; + if (isEthereumProtocolChain(chainName)) { + chainSigner = validator; + } else { + debugLog(`Retrieving GCP key for ${chainName}, as it is not EVM`); + chainSigner = new AgentGCPKey( + agentConfig.runEnv, + agentConfig.context, + Role.Validator, + chainName, + index, + ); + } return { validator, @@ -302,6 +316,7 @@ export function getValidatorKeysForChain( export async function createAgentKeysIfNotExists( agentConfig: AgentContextConfig, ) { + debugLog('Creating agent keys if none exist'); const keys = getAllCloudAgentKeys(agentConfig); await Promise.all( @@ -318,6 +333,7 @@ export async function createAgentKeysIfNotExists( } export async function deleteAgentKeys(agentConfig: AgentContextConfig) { + debugLog('Deleting agent keys'); const keys = getAllCloudAgentKeys(agentConfig); await Promise.all(keys.map((key) => key.delete())); await execCmd( @@ -333,6 +349,7 @@ export async function rotateKey( role: Role, chainName: ChainName, ) { + debugLog(`Rotating key for ${role} on ${chainName}`); const key = getCloudAgentKey(agentConfig, role, chainName); await key.update(); const keyIdentifier = key.identifier; @@ -357,6 +374,9 @@ async function persistAddresses( context: Contexts, keys: KeyAsAddress[], ) { + debugLog( + `Persisting addresses to GCP for ${context} context in ${environment} environment`, + ); await setGCPSecret( addressesIdentifier(environment, context), JSON.stringify(keys), @@ -371,6 +391,9 @@ async function fetchGCPKeyAddresses( environment: DeployEnvironment, context: Contexts, ) { + debugLog( + `Fetching addresses from GCP for ${context} context in ${environment} environment`, + ); const addresses = await fetchGCPSecret( addressesIdentifier(environment, context), ); diff --git a/typescript/infra/src/utils/gcloud.ts b/typescript/infra/src/utils/gcloud.ts index ef2650d357..dde9411ed0 100644 --- a/typescript/infra/src/utils/gcloud.ts +++ b/typescript/infra/src/utils/gcloud.ts @@ -1,3 +1,4 @@ +import debug from 'debug'; import fs from 'fs'; import { rm, writeFile } from 'fs/promises'; @@ -9,6 +10,8 @@ interface IamCondition { expression: string; } +const debugLog = debug('infra:utils:gcloud'); + // Allows secrets to be overridden via environment variables to avoid // gcloud calls. This is particularly useful for running commands in k8s, // where we can use external-secrets to fetch secrets from GCP secret manager, @@ -23,7 +26,7 @@ export async function fetchGCPSecret( const envVarOverride = tryGCPSecretFromEnvVariable(secretName); if (envVarOverride !== undefined) { - console.log( + debugLog( `Using environment variable instead of GCP secret with name ${secretName}`, ); output = envVarOverride; @@ -41,7 +44,7 @@ export async function fetchGCPSecret( // If the environment variable GCP_SECRET_OVERRIDES_ENABLED is `true`, // this will attempt to find an environment variable of the form: -// `GCP_SECRET_OVERRIDE_${gcpSecretName..replaceAll('-', '_').toUpperCase()}` +// `GCP_SECRET_OVERRIDE_${gcpSecretName.replaceAll('-', '_').toUpperCase()}` // If found, it's returned, otherwise, undefined is returned. function tryGCPSecretFromEnvVariable(gcpSecretName: string) { const overridingEnabled = @@ -58,9 +61,12 @@ function tryGCPSecretFromEnvVariable(gcpSecretName: string) { export async function gcpSecretExists(secretName: string) { const fullName = `projects/${await getCurrentProjectNumber()}/secrets/${secretName}`; + debugLog(`Checking if GCP secret exists for ${fullName}`); + const matches = await execCmdAndParseJson( `gcloud secrets list --filter name=${fullName} --format json`, ); + debugLog(`Matches: ${matches.length}`); return matches.length > 0; } @@ -80,10 +86,12 @@ export async function setGCPSecret( await execCmd( `gcloud secrets create ${secretName} --data-file=${fileName} --replication-policy=automatic --labels=${labelString}`, ); + debugLog(`Created new GCP secret for ${secretName}`); } else { await execCmd( `gcloud secrets versions add ${secretName} --data-file=${fileName}`, ); + debugLog(`Added new version to existing GCP secret for ${secretName}`); } await rm(fileName); } @@ -95,6 +103,9 @@ export async function createServiceAccountIfNotExists( let serviceAccountInfo = await getServiceAccountInfo(serviceAccountName); if (!serviceAccountInfo) { serviceAccountInfo = await createServiceAccount(serviceAccountName); + debugLog(`Created new service account with name ${serviceAccountName}`); + } else { + debugLog(`Service account with name ${serviceAccountName} already exists`); } return serviceAccountInfo.email; } @@ -110,6 +121,7 @@ export async function grantServiceAccountRoleIfNotExists( matchedBinding && iamConditionsEqual(condition, matchedBinding.condition) ) { + debugLog(`Service account ${serviceAccountEmail} already has role ${role}`); return; } await execCmd( @@ -119,6 +131,7 @@ export async function grantServiceAccountRoleIfNotExists( : '' }`, ); + debugLog(`Granted role ${role} to service account ${serviceAccountEmail}`); } export async function createServiceAccountKey(serviceAccountEmail: string) { @@ -128,12 +141,14 @@ export async function createServiceAccountKey(serviceAccountEmail: string) { ); const key = JSON.parse(fs.readFileSync(localKeyFile, 'utf8')); fs.rmSync(localKeyFile); + debugLog(`Created new service account key for ${serviceAccountEmail}`); return key; } // The alphanumeric project name / ID export async function getCurrentProject() { const [result] = await execCmd('gcloud config get-value project'); + debugLog(`Current GCP project ID: ${result.trim()}`); return result.trim(); } @@ -150,10 +165,12 @@ async function getIamMemberPolicyBindings(memberEmail: string) { const unprocessedRoles = await execCmdAndParseJson( `gcloud projects get-iam-policy $(gcloud config get-value project) --format "json(bindings)" --flatten="bindings[].members" --filter="bindings.members:${memberEmail}"`, ); - return unprocessedRoles.map((unprocessedRoleObject: any) => ({ + const bindings = unprocessedRoles.map((unprocessedRoleObject: any) => ({ role: unprocessedRoleObject.bindings.role, condition: unprocessedRoleObject.bindings.condition, })); + debugLog(`Retrieved IAM policy bindings for ${memberEmail}`); + return bindings; } async function createServiceAccount(serviceAccountName: string) { @@ -169,8 +186,10 @@ async function getServiceAccountInfo(serviceAccountName: string) { `gcloud iam service-accounts list --format json --filter displayName="${serviceAccountName}"`, ); if (matches.length === 0) { + debugLog(`No service account found with name ${serviceAccountName}`); return undefined; } + debugLog(`Found service account with name ${serviceAccountName}`); return matches[0]; } From 8907a7151ca1a8fb7938ba181a0707d3c5754872 Mon Sep 17 00:00:00 2001 From: Paul Balaji Date: Thu, 1 Feb 2024 18:17:09 +0000 Subject: [PATCH 2/9] feat: build head of PRs instead of merge commit (#3206) --- .github/workflows/agent-release-artifacts.yml | 2 +- .github/workflows/monorepo-docker.yml | 1 + .github/workflows/rust-docker.yml | 2 ++ .github/workflows/rust.yml | 4 ++++ .github/workflows/static-analysis.yml | 1 + .github/workflows/storage-analysis.yml | 2 +- .github/workflows/test.yml | 21 +++++++++++++------ 7 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.github/workflows/agent-release-artifacts.yml b/.github/workflows/agent-release-artifacts.yml index 79c421f22a..2827120989 100644 --- a/.github/workflows/agent-release-artifacts.yml +++ b/.github/workflows/agent-release-artifacts.yml @@ -49,7 +49,7 @@ jobs: run: | sudo apt-get update -qq sudo apt-get install -qq crossbuild-essential-arm64 crossbuild-essential-armhf - + # some additional configuration for cross-compilation on linux cat >>~/.cargo/config < Date: Fri, 2 Feb 2024 15:18:43 -0500 Subject: [PATCH 3/9] feat:removing igp from preset hook config (#3215) ### Description - To simplify the PI deployment, I've removed the igp from preset hook config which PI deployers use if they don't provide a hook config via the CLI. - This means we don't need to deploy IGP or storage gas oracles and the PI deployer can spin up relayer with gas enforcement policy set to false. ### Drive-by changes None ### Related issues None ### Backward compatibility Yes ### Testing Manual --- .changeset/friendly-peas-roll.md | 5 +++ typescript/cli/src/config/hooks.ts | 54 ++---------------------------- typescript/cli/src/deploy/core.ts | 11 +----- 3 files changed, 8 insertions(+), 62 deletions(-) create mode 100644 .changeset/friendly-peas-roll.md diff --git a/.changeset/friendly-peas-roll.md b/.changeset/friendly-peas-roll.md new file mode 100644 index 0000000000..d21ea273ec --- /dev/null +++ b/.changeset/friendly-peas-roll.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': patch +--- + +Removed IGP from preset hook config diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index 6392d60f95..0c0f49111c 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -9,10 +9,7 @@ import { GasOracleContractType, HookType, HooksConfig, - MultisigConfig, chainMetadata, - defaultMultisigConfigs, - multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; import { Address, @@ -83,41 +80,7 @@ export function isValidHookConfigMap(config: any) { return HooksConfigMapSchema.safeParse(config).success; } -export function presetHookConfigs( - owner: Address, - local: ChainName, - destinationChains: ChainName[], - multisigConfig?: MultisigConfig, -): HooksConfig { - const gasOracleType = destinationChains.reduce< - ChainMap - >((acc, chain) => { - acc[chain] = GasOracleContractType.StorageGasOracle; - return acc; - }, {}); - const overhead = destinationChains.reduce>((acc, chain) => { - let validatorThreshold: number; - let validatorCount: number; - if (multisigConfig) { - validatorThreshold = multisigConfig.threshold; - validatorCount = multisigConfig.validators.length; - } else if (local in defaultMultisigConfigs) { - validatorThreshold = defaultMultisigConfigs[local].threshold; - validatorCount = defaultMultisigConfigs[local].validators.length; - } else { - // default values - // fix here: https://github.com/hyperlane-xyz/issues/issues/773 - validatorThreshold = 2; - validatorCount = 3; - } - acc[chain] = multisigIsmVerificationCost( - validatorThreshold, - validatorCount, - ); - return acc; - }, {}); - - // TODO improve types here to avoid need for `as` casts +export function presetHookConfigs(owner: Address): HooksConfig { return { required: { type: HookType.PROTOCOL_FEE, @@ -127,20 +90,7 @@ export function presetHookConfigs( owner: owner, }, default: { - type: HookType.AGGREGATION, - hooks: [ - { - type: HookType.MERKLE_TREE, - }, - { - type: HookType.INTERCHAIN_GAS_PAYMASTER, - owner: owner, - beneficiary: owner, - gasOracleType, - overhead, - oracleKey: owner, - }, - ], + type: HookType.MERKLE_TREE, }, }; } diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 5f9537e132..6295632aca 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -328,7 +328,6 @@ async function executeDeploy({ chains, defaultIsms, hooksConfig, - multisigConfigs, ); const coreContracts = await coreDeployer.deploy(coreConfigs); @@ -391,17 +390,9 @@ function buildCoreConfigMap( chains: ChainName[], defaultIsms: ChainMap, hooksConfig: ChainMap, - multisigConfigs: ChainMap, ): ChainMap { return chains.reduce>((config, chain) => { - const hooks = - hooksConfig[chain] ?? - presetHookConfigs( - owner, - chain, - chains.filter((c) => c !== chain), - multisigConfigs[chain], // if no multisig config, uses default 2/3 - ); + const hooks = hooksConfig[chain] ?? presetHookConfigs(owner); config[chain] = { owner, defaultIsm: defaultIsms[chain], From 0865c948c378253b462d7be4fa33cd6e3d504cf0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Feb 2024 05:42:50 +0000 Subject: [PATCH 4/9] Version Packages (#3217) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @hyperlane-xyz/core@3.6.2 ### Patch Changes - @hyperlane-xyz/utils@3.6.2 ## @hyperlane-xyz/cli@3.6.2 ### Patch Changes - 99fe93a5b: Removed IGP from preset hook config - @hyperlane-xyz/sdk@3.6.2 - @hyperlane-xyz/utils@3.6.2 ## @hyperlane-xyz/helloworld@3.6.2 ### Patch Changes - @hyperlane-xyz/core@3.6.2 - @hyperlane-xyz/sdk@3.6.2 ## @hyperlane-xyz/sdk@3.6.2 ### Patch Changes - @hyperlane-xyz/core@3.6.2 - @hyperlane-xyz/utils@3.6.2 ## @hyperlane-xyz/utils@3.6.2 ## @hyperlane-xyz/infra@3.6.2 ### Patch Changes - @hyperlane-xyz/helloworld@3.6.2 - @hyperlane-xyz/sdk@3.6.2 - @hyperlane-xyz/utils@3.6.2 Co-authored-by: github-actions[bot] --- .changeset/friendly-peas-roll.md | 5 ----- solidity/CHANGELOG.md | 6 ++++++ solidity/package.json | 4 ++-- typescript/cli/CHANGELOG.md | 8 ++++++++ typescript/cli/package.json | 6 +++--- typescript/cli/src/version.ts | 2 +- typescript/helloworld/CHANGELOG.md | 7 +++++++ typescript/helloworld/package.json | 6 +++--- typescript/infra/CHANGELOG.md | 8 ++++++++ typescript/infra/package.json | 8 ++++---- typescript/sdk/CHANGELOG.md | 7 +++++++ typescript/sdk/package.json | 6 +++--- typescript/utils/CHANGELOG.md | 2 ++ typescript/utils/package.json | 2 +- yarn.lock | 28 ++++++++++++++-------------- 15 files changed, 69 insertions(+), 36 deletions(-) delete mode 100644 .changeset/friendly-peas-roll.md diff --git a/.changeset/friendly-peas-roll.md b/.changeset/friendly-peas-roll.md deleted file mode 100644 index d21ea273ec..0000000000 --- a/.changeset/friendly-peas-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/cli': patch ---- - -Removed IGP from preset hook config diff --git a/solidity/CHANGELOG.md b/solidity/CHANGELOG.md index cf7b6a84e6..fa903faa7c 100644 --- a/solidity/CHANGELOG.md +++ b/solidity/CHANGELOG.md @@ -1,5 +1,11 @@ # @hyperlane-xyz/core +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/solidity/package.json b/solidity/package.json index 102273e354..f3a6bb2b2d 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/core", "description": "Core solidity contracts for Hyperlane", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@eth-optimism/contracts": "^0.6.0", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/utils": "3.6.2", "@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts-upgradeable": "^v4.9.3" }, diff --git a/typescript/cli/CHANGELOG.md b/typescript/cli/CHANGELOG.md index d85ed3175d..ec9579f252 100644 --- a/typescript/cli/CHANGELOG.md +++ b/typescript/cli/CHANGELOG.md @@ -1,5 +1,13 @@ # @hyperlane-xyz/cli +## 3.6.2 + +### Patch Changes + +- 99fe93a5b: Removed IGP from preset hook config + - @hyperlane-xyz/sdk@3.6.2 + - @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/cli/package.json b/typescript/cli/package.json index e2b5c9bda0..3e366d04c8 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/cli", - "version": "3.6.1", + "version": "3.6.2", "description": "A command-line utility for common Hyperlane operations", "dependencies": { - "@hyperlane-xyz/sdk": "3.6.1", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/sdk": "3.6.2", + "@hyperlane-xyz/utils": "3.6.2", "@inquirer/prompts": "^3.0.0", "bignumber.js": "^9.1.1", "chalk": "^5.3.0", diff --git a/typescript/cli/src/version.ts b/typescript/cli/src/version.ts index 86c2700664..658ea336e5 100644 --- a/typescript/cli/src/version.ts +++ b/typescript/cli/src/version.ts @@ -1 +1 @@ -export const VERSION = '3.6.1'; +export const VERSION = '3.6.2'; diff --git a/typescript/helloworld/CHANGELOG.md b/typescript/helloworld/CHANGELOG.md index 992eedca50..40cff8da4b 100644 --- a/typescript/helloworld/CHANGELOG.md +++ b/typescript/helloworld/CHANGELOG.md @@ -1,5 +1,12 @@ # @hyperlane-xyz/helloworld +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/core@3.6.2 +- @hyperlane-xyz/sdk@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/helloworld/package.json b/typescript/helloworld/package.json index 773687366c..ba7ca9c7fc 100644 --- a/typescript/helloworld/package.json +++ b/typescript/helloworld/package.json @@ -1,10 +1,10 @@ { "name": "@hyperlane-xyz/helloworld", "description": "A basic skeleton of an Hyperlane app", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { - "@hyperlane-xyz/core": "3.6.1", - "@hyperlane-xyz/sdk": "3.6.1", + "@hyperlane-xyz/core": "3.6.2", + "@hyperlane-xyz/sdk": "3.6.2", "@openzeppelin/contracts-upgradeable": "^4.9.3", "ethers": "^5.7.2" }, diff --git a/typescript/infra/CHANGELOG.md b/typescript/infra/CHANGELOG.md index 7d1fb96962..7e7210dca9 100644 --- a/typescript/infra/CHANGELOG.md +++ b/typescript/infra/CHANGELOG.md @@ -1,5 +1,13 @@ # @hyperlane-xyz/infra +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/helloworld@3.6.2 +- @hyperlane-xyz/sdk@3.6.2 +- @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/infra/package.json b/typescript/infra/package.json index fe04c9a5ee..2059a0221a 100644 --- a/typescript/infra/package.json +++ b/typescript/infra/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/infra", "description": "Infrastructure utilities for the Hyperlane Network", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@arbitrum/sdk": "^3.0.0", "@aws-sdk/client-iam": "^3.74.0", @@ -12,9 +12,9 @@ "@ethersproject/experimental": "^5.7.0", "@ethersproject/hardware-wallets": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@hyperlane-xyz/helloworld": "3.6.1", - "@hyperlane-xyz/sdk": "3.6.1", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/helloworld": "3.6.2", + "@hyperlane-xyz/sdk": "3.6.2", + "@hyperlane-xyz/utils": "3.6.2", "@nomiclabs/hardhat-etherscan": "^3.0.3", "@safe-global/api-kit": "^1.3.0", "@safe-global/protocol-kit": "^1.2.0", diff --git a/typescript/sdk/CHANGELOG.md b/typescript/sdk/CHANGELOG.md index e90a804e6d..d4a314049b 100644 --- a/typescript/sdk/CHANGELOG.md +++ b/typescript/sdk/CHANGELOG.md @@ -1,5 +1,12 @@ # @hyperlane-xyz/sdk +## 3.6.2 + +### Patch Changes + +- @hyperlane-xyz/core@3.6.2 +- @hyperlane-xyz/utils@3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index bf03327d05..7d526909d8 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -1,12 +1,12 @@ { "name": "@hyperlane-xyz/sdk", "description": "The official SDK for the Hyperlane Network", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@cosmjs/cosmwasm-stargate": "^0.31.3", "@cosmjs/stargate": "^0.31.3", - "@hyperlane-xyz/core": "3.6.1", - "@hyperlane-xyz/utils": "3.6.1", + "@hyperlane-xyz/core": "3.6.2", + "@hyperlane-xyz/utils": "3.6.2", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.78.0", "@types/coingecko-api": "^1.0.10", diff --git a/typescript/utils/CHANGELOG.md b/typescript/utils/CHANGELOG.md index bacd69aaf4..57f4c424d6 100644 --- a/typescript/utils/CHANGELOG.md +++ b/typescript/utils/CHANGELOG.md @@ -1,5 +1,7 @@ # @hyperlane-xyz/utils +## 3.6.2 + ## 3.6.1 ### Patch Changes diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 9f88525122..a578e30cb8 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -1,7 +1,7 @@ { "name": "@hyperlane-xyz/utils", "description": "General utilities and types for the Hyperlane network", - "version": "3.6.1", + "version": "3.6.2", "dependencies": { "@cosmjs/encoding": "^0.31.3", "@solana/web3.js": "^1.78.0", diff --git a/yarn.lock b/yarn.lock index 34d6186d52..e2c15d053e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4235,8 +4235,8 @@ __metadata: version: 0.0.0-use.local resolution: "@hyperlane-xyz/cli@workspace:typescript/cli" dependencies: - "@hyperlane-xyz/sdk": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/sdk": "npm:3.6.2" + "@hyperlane-xyz/utils": "npm:3.6.2" "@inquirer/prompts": "npm:^3.0.0" "@types/mocha": "npm:^10.0.1" "@types/node": "npm:^18.14.5" @@ -4261,12 +4261,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/core@npm:3.6.1, @hyperlane-xyz/core@workspace:solidity": +"@hyperlane-xyz/core@npm:3.6.2, @hyperlane-xyz/core@workspace:solidity": version: 0.0.0-use.local resolution: "@hyperlane-xyz/core@workspace:solidity" dependencies: "@eth-optimism/contracts": "npm:^0.6.0" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/utils": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts": "npm:^4.9.3" @@ -4293,12 +4293,12 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/helloworld@npm:3.6.1, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": +"@hyperlane-xyz/helloworld@npm:3.6.2, @hyperlane-xyz/helloworld@workspace:typescript/helloworld": version: 0.0.0-use.local resolution: "@hyperlane-xyz/helloworld@workspace:typescript/helloworld" dependencies: - "@hyperlane-xyz/core": "npm:3.6.1" - "@hyperlane-xyz/sdk": "npm:3.6.1" + "@hyperlane-xyz/core": "npm:3.6.2" + "@hyperlane-xyz/sdk": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts-upgradeable": "npm:^4.9.3" @@ -4343,9 +4343,9 @@ __metadata: "@ethersproject/experimental": "npm:^5.7.0" "@ethersproject/hardware-wallets": "npm:^5.7.0" "@ethersproject/providers": "npm:^5.7.2" - "@hyperlane-xyz/helloworld": "npm:3.6.1" - "@hyperlane-xyz/sdk": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/helloworld": "npm:3.6.2" + "@hyperlane-xyz/sdk": "npm:3.6.2" + "@hyperlane-xyz/utils": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-etherscan": "npm:^3.0.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" @@ -4393,14 +4393,14 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/sdk@npm:3.6.1, @hyperlane-xyz/sdk@workspace:typescript/sdk": +"@hyperlane-xyz/sdk@npm:3.6.2, @hyperlane-xyz/sdk@workspace:typescript/sdk": version: 0.0.0-use.local resolution: "@hyperlane-xyz/sdk@workspace:typescript/sdk" dependencies: "@cosmjs/cosmwasm-stargate": "npm:^0.31.3" "@cosmjs/stargate": "npm:^0.31.3" - "@hyperlane-xyz/core": "npm:3.6.1" - "@hyperlane-xyz/utils": "npm:3.6.1" + "@hyperlane-xyz/core": "npm:3.6.2" + "@hyperlane-xyz/utils": "npm:3.6.2" "@nomiclabs/hardhat-ethers": "npm:^2.2.1" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@solana/spl-token": "npm:^0.3.8" @@ -4437,7 +4437,7 @@ __metadata: languageName: unknown linkType: soft -"@hyperlane-xyz/utils@npm:3.6.1, @hyperlane-xyz/utils@workspace:typescript/utils": +"@hyperlane-xyz/utils@npm:3.6.2, @hyperlane-xyz/utils@workspace:typescript/utils": version: 0.0.0-use.local resolution: "@hyperlane-xyz/utils@workspace:typescript/utils" dependencies: From d1621b49d010f4f50a88bdb763a4a4f9f7772efe Mon Sep 17 00:00:00 2001 From: Trevor Porter Date: Mon, 5 Feb 2024 10:56:06 +0000 Subject: [PATCH 5/9] Add Neutron back to mainnet3_config.json (#3220) ### Description Looks like https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3179/files#diff-edae1a4e631a67121d92119998e37740c0190bec5060e3c842cc86e4d3d26a4dL417 removed Neutron from mainnet3_config.json :( Seems like the Typescript tooling may have overridden it accidentally? Will need to figure out what to do there ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- rust/config/mainnet3_config.json | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/rust/config/mainnet3_config.json b/rust/config/mainnet3_config.json index 6c91592fb7..65e0e8ebd7 100644 --- a/rust/config/mainnet3_config.json +++ b/rust/config/mainnet3_config.json @@ -465,6 +465,42 @@ "from": 437300 } }, + "neutron": { + "name": "neutron", + "domainId": "1853125230", + "chainId": "neutron-1", + "mailbox": "0x848426d50eb2104d5c6381ec63757930b1c14659c40db8b8081e516e7c5238fc", + "interchainGasPaymaster": "0x504ee9ac43ec5814e00c7d21869a90ec52becb489636bdf893b7df9d606b5d67", + "validatorAnnounce": "0xf3aa0d652226e21ae35cd9035c492ae41725edc9036edf0d6a48701b153b90a0", + "merkleTreeHook": "0xcd30a0001cc1f436c41ef764a712ebabc5a144140e3fd03eafe64a9a24e4e27c", + "protocol": "cosmos", + "finalityBlocks": 1, + "rpcUrls": [ + { + "http": "https://rpc-kralum.neutron-1.neutron.org" + } + ], + "grpcUrl": "https://grpc-kralum.neutron-1.neutron.org:80", + "canonicalAsset": "untrn", + "bech32Prefix": "neutron", + "gasPrice": { + "amount": "0.57", + "denom": "untrn" + }, + "contractAddressBytes": 32, + "index": { + "from": 4000000, + "chunk": 100000 + }, + "blocks": { + "reorgPeriod": 1 + }, + "signer": { + "type": "cosmosKey", + "key": "0x5486418967eabc770b0fcb995f7ef6d9a72f7fc195531ef76c5109f44f51af26", + "prefix": "neutron" + } + }, "injective": { "name": "injective", "domainId": "6909546", From 8bfa8c13cc1125eea85fab9b32058ddd9d6a5076 Mon Sep 17 00:00:00 2001 From: Paul Balaji Date: Mon, 5 Feb 2024 17:50:21 +0000 Subject: [PATCH 6/9] fix: agent config artifact includes only chains user deployed to (#3223) --- typescript/cli/src/deploy/core.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 6295632aca..14617f9b83 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -472,18 +472,20 @@ async function writeAgentConfig( multiProvider: MultiProvider, ) { const startBlocks: ChainMap = {}; + const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider); + for (const chain of chains) { - const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider); const mailbox = core.getContracts(chain).mailbox; startBlocks[chain] = (await mailbox.deployedBlock()).toNumber(); } + const mergedAddressesMap = objMerge( sdkContractAddressesMap, artifacts, ) as ChainMap; const agentConfig = buildAgentConfig( - Object.keys(mergedAddressesMap), + chains, // Use only the chains that were deployed to multiProvider, mergedAddressesMap, startBlocks, From f2a0e92c7ff35ae0fef17d10ec1e2c2713578d34 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Mon, 5 Feb 2024 19:11:57 +0000 Subject: [PATCH 7/9] Cosmos grpc fallbackprovider (#3139) ### Description Implements grpc fallback provider logic for cosmos - initially tried implementing the fallback provider deprioritization logic at middleware level like in the EVM. The difference between ethers and cosmrs is that in the latter, middleware can only live at the transport layer (`tower` crate level). - based on this github [issue](https://github.com/hyperium/tonic/issues/733), that actually doesn't look possible, because the http::Request type isn't `Clone` so it can't be submitted to multiple providers - ended up implementing the fallback provider at the application layer, by keeping an array of grpc channels - There is now a `call` method in `hyperlane_core::FallbackProvider` which I'm actually really happy with. This method handles the fallbackprovider-specific logic by taking in an async closure, running it on each provider, and iterating providers if the closure call fails. In `grpc.rs` you can see how this is slightly verbose but I think it's quite manageable. The only part that bugs me is having to duplicate `Pin::from(Box::from(future))`, but that's need afaict because the regular closure returns an anonymous type - adds `grpcUrls` and `customGrpcUrls` config items - tests the cosmos fallback provider e2e ### Drive-by changes ### Related issues - Fixes: https://github.com/hyperlane-xyz/issues/issues/998 ### Backward compatibility ### Testing --- rust/Cargo.lock | 22 +- rust/Cargo.toml | 2 +- rust/agents/relayer/Cargo.toml | 2 +- rust/agents/validator/Cargo.toml | 2 +- rust/chains/hyperlane-cosmos/Cargo.toml | 3 +- rust/chains/hyperlane-cosmos/src/error.rs | 7 + rust/chains/hyperlane-cosmos/src/lib.rs | 1 + .../src/payloads/aggregate_ism.rs | 4 +- .../hyperlane-cosmos/src/payloads/general.rs | 2 +- .../src/payloads/ism_routes.rs | 12 +- .../hyperlane-cosmos/src/payloads/mailbox.rs | 20 +- .../src/payloads/merkle_tree_hook.rs | 8 +- .../src/payloads/multisig_ism.rs | 4 +- .../src/payloads/validator_announce.rs | 6 +- .../hyperlane-cosmos/src/providers/grpc.rs | 349 ++++++++++++------ .../src/rpc_clients/fallback.rs | 140 +++++++ .../hyperlane-cosmos/src/rpc_clients/mod.rs | 3 + .../hyperlane-cosmos/src/trait_builder.rs | 11 +- rust/chains/hyperlane-ethereum/Cargo.toml | 2 +- .../src/rpc_clients/fallback.rs | 119 +++--- .../hyperlane-ethereum/src/trait_builder.rs | 9 +- rust/chains/hyperlane-fuel/Cargo.toml | 2 +- rust/chains/hyperlane-sealevel/Cargo.toml | 2 +- rust/config/mainnet3_config.json | 8 +- rust/ethers-prometheus/src/json_rpc_client.rs | 6 +- .../templates/external-secret.yaml | 6 +- .../src/settings/parser/connection_parser.rs | 19 +- .../hyperlane-base/src/settings/parser/mod.rs | 106 ++++-- rust/hyperlane-core/Cargo.toml | 2 + .../src/rpc_clients/fallback.rs | 171 ++++++++- rust/hyperlane-core/src/rpc_clients/mod.rs | 6 +- rust/utils/abigen/src/lib.rs | 1 + rust/utils/run-locally/src/cosmos/types.rs | 12 +- typescript/sdk/src/metadata/agentConfig.ts | 2 +- .../sdk/src/metadata/chainMetadataTypes.ts | 6 + 35 files changed, 766 insertions(+), 311 deletions(-) create mode 100644 rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs create mode 100644 rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8f3bf3e33a..ae5e616edf 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4145,7 +4145,7 @@ dependencies = [ "hyperlane-fuel", "hyperlane-sealevel", "hyperlane-test", - "itertools 0.11.0", + "itertools 0.12.0", "maplit", "paste", "prometheus", @@ -4192,7 +4192,7 @@ dependencies = [ "fixed-hash 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "getrandom 0.2.11", "hex 0.4.3", - "itertools 0.11.0", + "itertools 0.12.0", "num 0.4.1", "num-derive 0.4.1", "num-traits", @@ -4226,6 +4226,7 @@ dependencies = [ "hyperlane-core", "injective-protobuf", "injective-std", + "itertools 0.12.0", "once_cell", "protobuf", "ripemd", @@ -4438,7 +4439,7 @@ dependencies = [ "hyperlane-core", "hyperlane-sealevel-interchain-security-module-interface", "hyperlane-sealevel-message-recipient-interface", - "itertools 0.11.0", + "itertools 0.12.0", "log", "num-derive 0.4.1", "num-traits", @@ -4464,7 +4465,7 @@ dependencies = [ "hyperlane-sealevel-test-ism", "hyperlane-sealevel-test-send-receiver", "hyperlane-test-utils", - "itertools 0.11.0", + "itertools 0.12.0", "log", "num-derive 0.4.1", "num-traits", @@ -4981,15 +4982,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.0" @@ -6918,7 +6910,7 @@ dependencies = [ "hyperlane-core", "hyperlane-ethereum", "hyperlane-test", - "itertools 0.11.0", + "itertools 0.12.0", "num-derive 0.4.1", "num-traits", "once_cell", @@ -7565,7 +7557,7 @@ dependencies = [ "hyperlane-base", "hyperlane-core", "hyperlane-test", - "itertools 0.11.0", + "itertools 0.12.0", "migration", "num-bigint 0.4.4", "prometheus", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1310918ed4..1c9a2e2ff4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -97,7 +97,7 @@ hyper = "0.14" hyper-tls = "0.5.0" injective-protobuf = "0.2.2" injective-std = "0.1.5" -itertools = "0.11.0" +itertools = "*" jobserver = "=0.1.26" jsonrpc-core = "18.0" k256 = { version = "0.13.1", features = ["std", "ecdsa"] } diff --git a/rust/agents/relayer/Cargo.toml b/rust/agents/relayer/Cargo.toml index 0bd5697971..d6eb28a153 100644 --- a/rust/agents/relayer/Cargo.toml +++ b/rust/agents/relayer/Cargo.toml @@ -34,7 +34,7 @@ tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } tracing-futures.workspace = true tracing.workspace = true -hyperlane-core = { path = "../../hyperlane-core", features = ["agent"] } +hyperlane-core = { path = "../../hyperlane-core", features = ["agent", "fallback-provider"] } hyperlane-base = { path = "../../hyperlane-base" } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } diff --git a/rust/agents/validator/Cargo.toml b/rust/agents/validator/Cargo.toml index f562938db2..bfea4c16a3 100644 --- a/rust/agents/validator/Cargo.toml +++ b/rust/agents/validator/Cargo.toml @@ -24,7 +24,7 @@ tokio = { workspace = true, features = ["rt", "macros", "parking_lot"] } tracing-futures.workspace = true tracing.workspace = true -hyperlane-core = { path = "../../hyperlane-core", features = ["agent"] } +hyperlane-core = { path = "../../hyperlane-core", features = ["agent", "fallback-provider"] } hyperlane-base = { path = "../../hyperlane-base" } hyperlane-ethereum = { path = "../../chains/hyperlane-ethereum" } hyperlane-cosmos = { path = "../../chains/hyperlane-cosmos" } diff --git a/rust/chains/hyperlane-cosmos/Cargo.toml b/rust/chains/hyperlane-cosmos/Cargo.toml index b48aa3590d..6115a61e6d 100644 --- a/rust/chains/hyperlane-cosmos/Cargo.toml +++ b/rust/chains/hyperlane-cosmos/Cargo.toml @@ -22,6 +22,7 @@ hyper = { workspace = true } hyper-tls = { workspace = true } injective-protobuf = { workspace = true } injective-std = { workspace = true } +itertools = { workspace = true } once_cell = { workspace = true } protobuf = { workspace = true } ripemd = { workspace = true } @@ -38,4 +39,4 @@ tracing = { workspace = true } tracing-futures = { workspace = true } url = { workspace = true } -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["fallback-provider"]} diff --git a/rust/chains/hyperlane-cosmos/src/error.rs b/rust/chains/hyperlane-cosmos/src/error.rs index 2c3e1e7475..06fffaff7e 100644 --- a/rust/chains/hyperlane-cosmos/src/error.rs +++ b/rust/chains/hyperlane-cosmos/src/error.rs @@ -1,5 +1,6 @@ use cosmrs::proto::prost; use hyperlane_core::ChainCommunicationError; +use std::fmt::Debug; /// Errors from the crates specific to the hyperlane-cosmos /// implementation. @@ -28,6 +29,9 @@ pub enum HyperlaneCosmosError { /// Tonic error #[error("{0}")] Tonic(#[from] tonic::transport::Error), + /// Tonic codegen error + #[error("{0}")] + TonicGenError(#[from] tonic::codegen::StdError), /// Tendermint RPC Error #[error(transparent)] TendermintError(#[from] tendermint_rpc::error::Error), @@ -37,6 +41,9 @@ pub enum HyperlaneCosmosError { /// Protobuf error #[error("{0}")] Protobuf(#[from] protobuf::ProtobufError), + /// Fallback providers failed + #[error("Fallback providers failed. (Errors: {0:?})")] + FallbackProvidersFailed(Vec), } impl From for ChainCommunicationError { diff --git a/rust/chains/hyperlane-cosmos/src/lib.rs b/rust/chains/hyperlane-cosmos/src/lib.rs index 82a4a0ece1..c0ce3ad549 100644 --- a/rust/chains/hyperlane-cosmos/src/lib.rs +++ b/rust/chains/hyperlane-cosmos/src/lib.rs @@ -16,6 +16,7 @@ mod multisig_ism; mod payloads; mod providers; mod routing_ism; +mod rpc_clients; mod signers; mod trait_builder; mod types; diff --git a/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs b/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs index 8276675ff7..23bb35a8f8 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/aggregate_ism.rs @@ -1,11 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyRequest { pub verify: VerifyRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyRequestInner { pub metadata: String, pub message: String, diff --git a/rust/chains/hyperlane-cosmos/src/payloads/general.rs b/rust/chains/hyperlane-cosmos/src/payloads/general.rs index 488cae2d37..af2a4b0b6e 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/general.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/general.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct EmptyStruct {} #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs b/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs index 052a1cc48b..1f659840e4 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs @@ -1,12 +1,12 @@ use super::general::EmptyStruct; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct IsmRouteRequest { pub route: IsmRouteRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct IsmRouteRequestInner { pub message: String, // hexbinary } @@ -16,22 +16,22 @@ pub struct IsmRouteRespnose { pub ism: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryRoutingIsmGeneralRequest { pub routing_ism: T, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryRoutingIsmRouteResponse { pub ism: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryIsmGeneralRequest { pub ism: T, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct QueryIsmModuleTypeRequest { pub module_type: EmptyStruct, } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs b/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs index 145ba5b16c..75eef04595 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/mailbox.rs @@ -3,52 +3,52 @@ use serde::{Deserialize, Serialize}; use super::general::EmptyStruct; // Requests -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GeneralMailboxQuery { pub mailbox: T, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct CountRequest { pub count: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct NonceRequest { pub nonce: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct RecipientIsmRequest { pub recipient_ism: RecipientIsmRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct RecipientIsmRequestInner { pub recipient_addr: String, // hexbinary } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DefaultIsmRequest { pub default_ism: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DeliveredRequest { pub message_delivered: DeliveredRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct DeliveredRequestInner { pub id: String, // hexbinary } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessMessageRequest { pub process: ProcessMessageRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ProcessMessageRequestInner { pub metadata: String, pub message: String, diff --git a/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs b/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs index 7635f0ef72..e960628771 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/merkle_tree_hook.rs @@ -4,24 +4,24 @@ use super::general::EmptyStruct; const TREE_DEPTH: usize = 32; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct MerkleTreeGenericRequest { pub merkle_hook: T, } // --------- Requests --------- -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct MerkleTreeRequest { pub tree: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct MerkleTreeCountRequest { pub count: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct CheckPointRequest { pub check_point: EmptyStruct, } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs b/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs index 204e726dc7..c56588d1d6 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/multisig_ism.rs @@ -1,11 +1,11 @@ use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyInfoRequest { pub verify_info: VerifyInfoRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifyInfoRequestInner { pub message: String, // hexbinary } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs b/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs index fdf449c7c4..cf4e5eb1f8 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/validator_announce.rs @@ -2,17 +2,17 @@ use serde::{Deserialize, Serialize}; use super::general::EmptyStruct; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetAnnouncedValidatorsRequest { pub get_announced_validators: EmptyStruct, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetAnnounceStorageLocationsRequest { pub get_announce_storage_locations: GetAnnounceStorageLocationsRequestInner, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetAnnounceStorageLocationsRequestInner { pub validators: Vec, } diff --git a/rust/chains/hyperlane-cosmos/src/providers/grpc.rs b/rust/chains/hyperlane-cosmos/src/providers/grpc.rs index 3594c7398e..a6bc070aba 100644 --- a/rust/chains/hyperlane-cosmos/src/providers/grpc.rs +++ b/rust/chains/hyperlane-cosmos/src/providers/grpc.rs @@ -24,7 +24,9 @@ use cosmrs::{ tx::{self, Fee, MessageExt, SignDoc, SignerInfo}, Any, Coin, }; +use derive_new::new; use hyperlane_core::{ + rpc_clients::{BlockNumberGetter, FallbackProvider}, ChainCommunicationError, ChainResult, ContractLocator, FixedPointNumber, HyperlaneDomain, U256, }; use protobuf::Message as _; @@ -33,9 +35,10 @@ use tonic::{ transport::{Channel, Endpoint}, GrpcMethod, IntoRequest, }; +use url::Url; -use crate::HyperlaneCosmosError; use crate::{address::CosmosAddress, CosmosAmount}; +use crate::{rpc_clients::CosmosFallbackProvider, HyperlaneCosmosError}; use crate::{signers::Signer, ConnectionConf}; /// A multiplier applied to a simulated transaction's gas usage to @@ -45,6 +48,36 @@ const GAS_ESTIMATE_MULTIPLIER: f64 = 1.25; /// be valid for. const TIMEOUT_BLOCKS: u64 = 1000; +#[derive(Debug, Clone, new)] +struct CosmosChannel { + channel: Channel, + /// The url that this channel is connected to. + /// Not explicitly used, but useful for debugging. + _url: Url, +} + +#[async_trait] +impl BlockNumberGetter for CosmosChannel { + async fn get_block_number(&self) -> Result { + let mut client = ServiceClient::new(self.channel.clone()); + let request = tonic::Request::new(GetLatestBlockRequest {}); + + let response = client + .get_latest_block(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + let height = response + .block + .ok_or_else(|| ChainCommunicationError::from_other_str("block not present"))? + .header + .ok_or_else(|| ChainCommunicationError::from_other_str("header not present"))? + .height; + + Ok(height as u64) + } +} + #[async_trait] /// Cosmwasm GRPC Provider pub trait WasmProvider: Send + Sync { @@ -56,14 +89,14 @@ pub trait WasmProvider: Send + Sync { async fn latest_block_height(&self) -> ChainResult; /// Perform a wasm query against the stored contract address. - async fn wasm_query( + async fn wasm_query( &self, payload: T, block_height: Option, ) -> ChainResult>; /// Perform a wasm query against a specified contract address. - async fn wasm_query_to( + async fn wasm_query_to( &self, to: String, payload: T, @@ -71,14 +104,17 @@ pub trait WasmProvider: Send + Sync { ) -> ChainResult>; /// Send a wasm tx. - async fn wasm_send( + async fn wasm_send( &self, payload: T, gas_limit: Option, ) -> ChainResult; /// Estimate gas for a wasm tx. - async fn wasm_estimate_gas(&self, payload: T) -> ChainResult; + async fn wasm_estimate_gas( + &self, + payload: T, + ) -> ChainResult; } #[derive(Debug, Clone)] @@ -95,7 +131,7 @@ pub struct WasmGrpcProvider { signer: Option, /// GRPC Channel that can be cheaply cloned. /// See `` - channel: Channel, + provider: CosmosFallbackProvider, gas_price: CosmosAmount, } @@ -108,9 +144,21 @@ impl WasmGrpcProvider { locator: Option, signer: Option, ) -> ChainResult { - let endpoint = - Endpoint::new(conf.get_grpc_url()).map_err(Into::::into)?; - let channel = endpoint.connect_lazy(); + // get all the configured grpc urls and convert them to a Vec + let channels: Result, _> = conf + .get_grpc_urls() + .into_iter() + .map(|url| { + Endpoint::new(url.to_string()) + .map(|e| CosmosChannel::new(e.connect_lazy(), url)) + .map_err(Into::::into) + }) + .collect(); + let mut builder = FallbackProvider::builder(); + builder = builder.add_providers(channels?); + let fallback_provider = builder.build(); + let provider = CosmosFallbackProvider::new(fallback_provider); + let contract_address = locator .map(|l| { CosmosAddress::from_h256( @@ -126,7 +174,7 @@ impl WasmGrpcProvider { conf, contract_address, signer, - channel, + provider, gas_price, }) } @@ -225,21 +273,36 @@ impl WasmGrpcProvider { // https://github.com/cosmos/cosmjs/blob/44893af824f0712d1f406a8daa9fcae335422235/packages/stargate/src/modules/tx/queries.ts#L67 signatures: vec![vec![]], }; - - let mut client = TxServiceClient::new(self.channel.clone()); let tx_bytes = raw_tx .to_bytes() .map_err(ChainCommunicationError::from_other)?; - #[allow(deprecated)] - let sim_req = tonic::Request::new(SimulateRequest { tx: None, tx_bytes }); - let gas_used = client - .simulate(sim_req) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner() - .gas_info - .ok_or_else(|| ChainCommunicationError::from_other_str("gas info not present"))? - .gas_used; + let gas_used = self + .provider + .call(move |provider| { + let tx_bytes_clone = tx_bytes.clone(); + let future = async move { + let mut client = TxServiceClient::new(provider.channel.clone()); + #[allow(deprecated)] + let sim_req = tonic::Request::new(SimulateRequest { + tx: None, + tx_bytes: tx_bytes_clone, + }); + let gas_used = client + .simulate(sim_req) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner() + .gas_info + .ok_or_else(|| { + ChainCommunicationError::from_other_str("gas info not present") + })? + .gas_used; + + Ok(gas_used) + }; + Box::pin(future) + }) + .await?; let gas_estimate = (gas_used as f64 * GAS_ESTIMATE_MULTIPLIER) as u64; @@ -248,14 +311,25 @@ impl WasmGrpcProvider { /// Fetches balance for a given `address` and `denom` pub async fn get_balance(&self, address: String, denom: String) -> ChainResult { - let mut client = QueryBalanceClient::new(self.channel.clone()); - - let balance_request = tonic::Request::new(QueryBalanceRequest { address, denom }); - let response = client - .balance(balance_request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); + let response = self + .provider + .call(move |provider| { + let address = address.clone(); + let denom = denom.clone(); + let future = async move { + let mut client = QueryBalanceClient::new(provider.channel.clone()); + let balance_request = + tonic::Request::new(QueryBalanceRequest { address, denom }); + let response = client + .balance(balance_request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; let balance = response .balance @@ -272,14 +346,23 @@ impl WasmGrpcProvider { return self.account_query_injective(account).await; } - let mut client = QueryAccountClient::new(self.channel.clone()); - - let request = tonic::Request::new(QueryAccountRequest { address: account }); - let response = client - .account(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); + let response = self + .provider + .call(move |provider| { + let address = account.clone(); + let future = async move { + let mut client = QueryAccountClient::new(provider.channel.clone()); + let request = tonic::Request::new(QueryAccountRequest { address }); + let response = client + .account(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; let account = BaseAccount::decode( response @@ -294,32 +377,46 @@ impl WasmGrpcProvider { /// Injective-specific logic for querying an account. async fn account_query_injective(&self, account: String) -> ChainResult { - let request = tonic::Request::new( - injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest { address: account }, - ); - - // Borrowed from the logic of `QueryAccountClient` in `cosmrs`, but using injective types. - - let mut grpc_client = tonic::client::Grpc::new(self.channel.clone()); - grpc_client - .ready() - .await - .map_err(Into::::into)?; - - let codec = tonic::codec::ProstCodec::default(); - let path = http::uri::PathAndQuery::from_static("/cosmos.auth.v1beta1.Query/Account"); - let mut req: tonic::Request< - injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest, - > = request.into_request(); - req.extensions_mut() - .insert(GrpcMethod::new("cosmos.auth.v1beta1.Query", "Account")); - - let response: tonic::Response< - injective_std::types::cosmos::auth::v1beta1::QueryAccountResponse, - > = grpc_client - .unary(req, path, codec) - .await - .map_err(Into::::into)?; + let response = self + .provider + .call(move |provider| { + let address = account.clone(); + let future = async move { + let request = tonic::Request::new( + injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest { + address, + }, + ); + + // Borrowed from the logic of `QueryAccountClient` in `cosmrs`, but using injective types. + + let mut grpc_client = tonic::client::Grpc::new(provider.channel.clone()); + grpc_client + .ready() + .await + .map_err(Into::::into)?; + + let codec = tonic::codec::ProstCodec::default(); + let path = + http::uri::PathAndQuery::from_static("/cosmos.auth.v1beta1.Query/Account"); + let mut req: tonic::Request< + injective_std::types::cosmos::auth::v1beta1::QueryAccountRequest, + > = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("cosmos.auth.v1beta1.Query", "Account")); + + let response: tonic::Response< + injective_std::types::cosmos::auth::v1beta1::QueryAccountResponse, + > = grpc_client + .unary(req, path, codec) + .await + .map_err(Into::::into)?; + + Ok(response) + }; + Box::pin(future) + }) + .await?; let mut eth_account = injective_protobuf::proto::account::EthAccount::parse_from_bytes( response @@ -349,14 +446,23 @@ impl WasmGrpcProvider { #[async_trait] impl WasmProvider for WasmGrpcProvider { async fn latest_block_height(&self) -> ChainResult { - let mut client = ServiceClient::new(self.channel.clone()); - let request = tonic::Request::new(GetLatestBlockRequest {}); + let response = self + .provider + .call(move |provider| { + let future = async move { + let mut client = ServiceClient::new(provider.channel.clone()); + let request = tonic::Request::new(GetLatestBlockRequest {}); + let response = client + .get_latest_block(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; - let response = client - .get_latest_block(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); let height = response .block .ok_or_else(|| ChainCommunicationError::from_other_str("block not present"))? @@ -369,7 +475,7 @@ impl WasmProvider for WasmGrpcProvider { async fn wasm_query(&self, payload: T, block_height: Option) -> ChainResult> where - T: Serialize + Send + Sync, + T: Serialize + Send + Sync + Clone, { let contract_address = self.contract_address.as_ref().ok_or_else(|| { ChainCommunicationError::from_other_str("No contract address available") @@ -385,39 +491,48 @@ impl WasmProvider for WasmGrpcProvider { block_height: Option, ) -> ChainResult> where - T: Serialize + Send + Sync, + T: Serialize + Send + Sync + Clone, { - let mut client = WasmQueryClient::new(self.channel.clone()); - let mut request = tonic::Request::new(QuerySmartContractStateRequest { - address: to, - query_data: serde_json::to_string(&payload)?.as_bytes().to_vec(), - }); - - if let Some(block_height) = block_height { - request - .metadata_mut() - .insert("x-cosmos-block-height", block_height.into()); - } - - let response = client - .smart_contract_state(request) - .await - .map_err(ChainCommunicationError::from_other)? - .into_inner(); + let query_data = serde_json::to_string(&payload)?.as_bytes().to_vec(); + let response = self + .provider + .call(move |provider| { + let to = to.clone(); + let query_data = query_data.clone(); + let future = async move { + let mut client = WasmQueryClient::new(provider.channel.clone()); + + let mut request = tonic::Request::new(QuerySmartContractStateRequest { + address: to, + query_data, + }); + if let Some(block_height) = block_height { + request + .metadata_mut() + .insert("x-cosmos-block-height", block_height.into()); + } + let response = client + .smart_contract_state(request) + .await + .map_err(ChainCommunicationError::from_other)? + .into_inner(); + Ok(response) + }; + Box::pin(future) + }) + .await?; Ok(response.data) } async fn wasm_send(&self, payload: T, gas_limit: Option) -> ChainResult where - T: Serialize + Send + Sync, + T: Serialize + Send + Sync + Clone, { let signer = self.get_signer()?; - let mut client = TxServiceClient::new(self.channel.clone()); let contract_address = self.contract_address.as_ref().ok_or_else(|| { ChainCommunicationError::from_other_str("No contract address available") })?; - let msgs = vec![MsgExecuteContract { sender: signer.address.clone(), contract: contract_address.address(), @@ -426,9 +541,6 @@ impl WasmProvider for WasmGrpcProvider { } .to_any() .map_err(ChainCommunicationError::from_other)?]; - - // We often use U256s to represent gas limits, but Cosmos expects u64s. Try to convert, - // and if it fails, just fallback to None which will result in gas estimation. let gas_limit: Option = gas_limit.and_then(|limit| match limit.try_into() { Ok(limit) => Some(limit), Err(err) => { @@ -439,20 +551,30 @@ impl WasmProvider for WasmGrpcProvider { None } }); - - let tx_req = BroadcastTxRequest { - tx_bytes: self.generate_raw_signed_tx(msgs, gas_limit).await?, - mode: BroadcastMode::Sync as i32, - }; - - let tx_res = client - .broadcast_tx(tx_req) - .await - .map_err(Into::::into)? - .into_inner() - .tx_response - .ok_or_else(|| ChainCommunicationError::from_other_str("Empty tx_response"))?; - + let tx_bytes = self.generate_raw_signed_tx(msgs, gas_limit).await?; + let tx_res = self + .provider + .call(move |provider| { + let tx_bytes = tx_bytes.clone(); + let future = async move { + let mut client = TxServiceClient::new(provider.channel.clone()); + // We often use U256s to represent gas limits, but Cosmos expects u64s. Try to convert, + // and if it fails, just fallback to None which will result in gas estimation. + let tx_req = BroadcastTxRequest { + tx_bytes, + mode: BroadcastMode::Sync as i32, + }; + client + .broadcast_tx(tx_req) + .await + .map_err(Into::::into)? + .into_inner() + .tx_response + .ok_or_else(|| ChainCommunicationError::from_other_str("Empty tx_response")) + }; + Box::pin(future) + }) + .await?; Ok(tx_res) } @@ -482,3 +604,10 @@ impl WasmProvider for WasmGrpcProvider { Ok(response) } } + +#[async_trait] +impl BlockNumberGetter for WasmGrpcProvider { + async fn get_block_number(&self) -> Result { + self.latest_block_height().await + } +} diff --git a/rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs b/rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs new file mode 100644 index 0000000000..cf933ee73d --- /dev/null +++ b/rust/chains/hyperlane-cosmos/src/rpc_clients/fallback.rs @@ -0,0 +1,140 @@ +use std::{ + fmt::{Debug, Formatter}, + ops::Deref, +}; + +use derive_new::new; +use hyperlane_core::rpc_clients::FallbackProvider; + +/// Wrapper of `FallbackProvider` for use in `hyperlane-cosmos` +#[derive(new, Clone)] +pub struct CosmosFallbackProvider { + fallback_provider: FallbackProvider, +} + +impl Deref for CosmosFallbackProvider { + type Target = FallbackProvider; + + fn deref(&self) -> &Self::Target { + &self.fallback_provider + } +} + +impl Debug for CosmosFallbackProvider +where + C: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.fallback_provider.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use async_trait::async_trait; + use hyperlane_core::rpc_clients::test::ProviderMock; + use hyperlane_core::rpc_clients::{BlockNumberGetter, FallbackProviderBuilder}; + use hyperlane_core::ChainCommunicationError; + use tokio::time::sleep; + + use super::*; + + #[derive(Debug, Clone)] + struct CosmosProviderMock(ProviderMock); + + impl Deref for CosmosProviderMock { + type Target = ProviderMock; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl Default for CosmosProviderMock { + fn default() -> Self { + Self(ProviderMock::default()) + } + } + + impl CosmosProviderMock { + fn new(request_sleep: Option) -> Self { + Self(ProviderMock::new(request_sleep)) + } + } + + #[async_trait] + impl BlockNumberGetter for CosmosProviderMock { + async fn get_block_number(&self) -> Result { + Ok(0) + } + } + + impl Into> for CosmosProviderMock { + fn into(self) -> Box { + Box::new(self) + } + } + + impl CosmosFallbackProvider { + async fn low_level_test_call(&mut self) -> Result<(), ChainCommunicationError> { + self.call(|provider| { + provider.push("GET", "http://localhost:1234"); + let future = async move { + let body = tonic::body::BoxBody::default(); + let response = http::Response::builder().status(200).body(body).unwrap(); + if let Some(sleep_duration) = provider.request_sleep() { + sleep(sleep_duration).await; + } + Ok(response) + }; + Box::pin(future) + }) + .await?; + Ok(()) + } + } + + #[tokio::test] + async fn test_first_provider_is_attempted() { + let fallback_provider_builder = FallbackProviderBuilder::default(); + let providers = vec![ + CosmosProviderMock::default(), + CosmosProviderMock::default(), + CosmosProviderMock::default(), + ]; + let fallback_provider = fallback_provider_builder.add_providers(providers).build(); + let mut cosmos_fallback_provider = CosmosFallbackProvider::new(fallback_provider); + cosmos_fallback_provider + .low_level_test_call() + .await + .unwrap(); + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(&cosmos_fallback_provider).await; + assert_eq!(provider_call_count, vec![1, 0, 0]); + } + + #[tokio::test] + async fn test_one_stalled_provider() { + let fallback_provider_builder = FallbackProviderBuilder::default(); + let providers = vec![ + CosmosProviderMock::new(Some(Duration::from_millis(10))), + CosmosProviderMock::default(), + CosmosProviderMock::default(), + ]; + let fallback_provider = fallback_provider_builder + .add_providers(providers) + .with_max_block_time(Duration::from_secs(0)) + .build(); + let mut cosmos_fallback_provider = CosmosFallbackProvider::new(fallback_provider); + cosmos_fallback_provider + .low_level_test_call() + .await + .unwrap(); + + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(&cosmos_fallback_provider).await; + assert_eq!(provider_call_count, vec![0, 0, 1]); + } +} diff --git a/rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs b/rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs new file mode 100644 index 0000000000..536845688d --- /dev/null +++ b/rust/chains/hyperlane-cosmos/src/rpc_clients/mod.rs @@ -0,0 +1,3 @@ +pub use self::fallback::*; + +mod fallback; diff --git a/rust/chains/hyperlane-cosmos/src/trait_builder.rs b/rust/chains/hyperlane-cosmos/src/trait_builder.rs index 2bacb4d2f5..1bb3627b9d 100644 --- a/rust/chains/hyperlane-cosmos/src/trait_builder.rs +++ b/rust/chains/hyperlane-cosmos/src/trait_builder.rs @@ -2,12 +2,13 @@ use std::str::FromStr; use derive_new::new; use hyperlane_core::{ChainCommunicationError, FixedPointNumber}; +use url::Url; /// Cosmos connection configuration #[derive(Debug, Clone)] pub struct ConnectionConf { /// The GRPC url to connect to - grpc_url: String, + grpc_urls: Vec, /// The RPC url to connect to rpc_url: String, /// The chain ID @@ -76,8 +77,8 @@ pub enum ConnectionConfError { impl ConnectionConf { /// Get the GRPC url - pub fn get_grpc_url(&self) -> String { - self.grpc_url.clone() + pub fn get_grpc_urls(&self) -> Vec { + self.grpc_urls.clone() } /// Get the RPC url @@ -112,7 +113,7 @@ impl ConnectionConf { /// Create a new connection configuration pub fn new( - grpc_url: String, + grpc_urls: Vec, rpc_url: String, chain_id: String, bech32_prefix: String, @@ -121,7 +122,7 @@ impl ConnectionConf { contract_address_bytes: usize, ) -> Self { Self { - grpc_url, + grpc_urls, rpc_url, chain_id, bech32_prefix, diff --git a/rust/chains/hyperlane-ethereum/Cargo.toml b/rust/chains/hyperlane-ethereum/Cargo.toml index 8d6db17f45..a72855a00b 100644 --- a/rust/chains/hyperlane-ethereum/Cargo.toml +++ b/rust/chains/hyperlane-ethereum/Cargo.toml @@ -30,7 +30,7 @@ tracing-futures.workspace = true tracing.workspace = true url.workspace = true -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["fallback-provider"]} ethers-prometheus = { path = "../../ethers-prometheus", features = ["serde"] } [build-dependencies] diff --git a/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs b/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs index 2ac6ae009f..1243ecc106 100644 --- a/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs +++ b/rust/chains/hyperlane-ethereum/src/rpc_clients/fallback.rs @@ -12,23 +12,23 @@ use serde_json::Value; use tokio::time::sleep; use tracing::{instrument, warn_span}; -use ethers_prometheus::json_rpc_client::PrometheusJsonRpcClientConfigExt; +use ethers_prometheus::json_rpc_client::{JsonRpcBlockGetter, PrometheusJsonRpcClientConfigExt}; use crate::rpc_clients::{categorize_client_response, CategorizedResponse}; /// Wrapper of `FallbackProvider` for use in `hyperlane-ethereum` #[derive(new)] -pub struct EthereumFallbackProvider(FallbackProvider); +pub struct EthereumFallbackProvider(FallbackProvider); -impl Deref for EthereumFallbackProvider { - type Target = FallbackProvider; +impl Deref for EthereumFallbackProvider { + type Target = FallbackProvider; fn deref(&self) -> &Self::Target { &self.0 } } -impl Debug for EthereumFallbackProvider +impl Debug for EthereumFallbackProvider where C: JsonRpcClient + PrometheusJsonRpcClientConfigExt, { @@ -73,16 +73,17 @@ impl From for ProviderError { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl JsonRpcClient for EthereumFallbackProvider +impl JsonRpcClient for EthereumFallbackProvider> where C: JsonRpcClient + + Into> + PrometheusJsonRpcClientConfigExt - + Into> + Clone, + JsonRpcBlockGetter: BlockNumberGetter, { type Error = ProviderError; - // TODO: Refactor the reusable parts of this function when implementing the cosmos-specific logic + // TODO: Refactor to use `FallbackProvider::call` #[instrument] async fn request(&self, method: &str, params: T) -> Result where @@ -108,7 +109,7 @@ where let resp = fut.await; self.handle_stalled_provider(priority, provider).await; let _span = - warn_span!("request_with_fallback", fallback_count=%idx, provider_index=%priority.index, ?provider).entered(); + warn_span!("request", fallback_count=%idx, provider_index=%priority.index, ?provider).entered(); match categorize_client_response(method, resp) { IsOk(v) => return Ok(serde_json::from_value(v)?), @@ -125,41 +126,37 @@ where #[cfg(test)] mod tests { use ethers_prometheus::json_rpc_client::{JsonRpcBlockGetter, BLOCK_NUMBER_RPC}; + use hyperlane_core::rpc_clients::test::ProviderMock; use hyperlane_core::rpc_clients::FallbackProviderBuilder; use super::*; - use std::sync::{Arc, Mutex}; #[derive(Debug, Clone)] - struct ProviderMock { - // Store requests as tuples of (method, params) - // Even if the tests were single-threaded, need the arc-mutex - // for interior mutability in `JsonRpcClient::request` - requests: Arc>>, - } + struct EthereumProviderMock(ProviderMock); - impl ProviderMock { - fn new() -> Self { - Self { - requests: Arc::new(Mutex::new(vec![])), - } + impl Deref for EthereumProviderMock { + type Target = ProviderMock; + + fn deref(&self) -> &Self::Target { + &self.0 } + } - fn push(&self, method: &str, params: T) { - self.requests - .lock() - .unwrap() - .push((method.to_owned(), format!("{:?}", params))); + impl Default for EthereumProviderMock { + fn default() -> Self { + Self(ProviderMock::default()) } + } - fn requests(&self) -> Vec<(String, String)> { - self.requests.lock().unwrap().clone() + impl EthereumProviderMock { + fn new(request_sleep: Option) -> Self { + Self(ProviderMock::new(request_sleep)) } } - impl Into> for ProviderMock { - fn into(self) -> Box { - Box::new(JsonRpcBlockGetter::new(self.clone())) + impl Into> for EthereumProviderMock { + fn into(self) -> JsonRpcBlockGetter { + JsonRpcBlockGetter::new(self) } } @@ -171,7 +168,7 @@ mod tests { } #[async_trait] - impl JsonRpcClient for ProviderMock { + impl JsonRpcClient for EthereumProviderMock { type Error = HttpClientError; /// Pushes the `(method, params)` to the back of the `requests` queue, @@ -182,12 +179,14 @@ mod tests { params: T, ) -> Result { self.push(method, params); - sleep(Duration::from_millis(10)).await; + if let Some(sleep_duration) = self.request_sleep() { + sleep(sleep_duration).await; + } dummy_return_value() } } - impl PrometheusJsonRpcClientConfigExt for ProviderMock { + impl PrometheusJsonRpcClientConfigExt for EthereumProviderMock { fn node_host(&self) -> &str { todo!() } @@ -197,35 +196,32 @@ mod tests { } } - async fn get_call_counts(fallback_provider: &FallbackProvider) -> Vec { - fallback_provider - .inner - .priorities - .read() - .await - .iter() - .map(|p| { - let provider = &fallback_provider.inner.providers[p.index]; - provider.requests().len() - }) - .collect() + impl EthereumFallbackProvider> + where + C: JsonRpcClient + + PrometheusJsonRpcClientConfigExt + + Into> + + Clone, + JsonRpcBlockGetter: BlockNumberGetter, + { + async fn low_level_test_call(&self) { + self.request::<_, u64>(BLOCK_NUMBER_RPC, ()).await.unwrap(); + } } #[tokio::test] async fn test_first_provider_is_attempted() { let fallback_provider_builder = FallbackProviderBuilder::default(); let providers = vec![ - ProviderMock::new(), - ProviderMock::new(), - ProviderMock::new(), + EthereumProviderMock::default(), + EthereumProviderMock::default(), + EthereumProviderMock::default(), ]; let fallback_provider = fallback_provider_builder.add_providers(providers).build(); let ethereum_fallback_provider = EthereumFallbackProvider::new(fallback_provider); - ethereum_fallback_provider - .request::<_, u64>(BLOCK_NUMBER_RPC, ()) - .await - .unwrap(); - let provider_call_count: Vec<_> = get_call_counts(ðereum_fallback_provider).await; + ethereum_fallback_provider.low_level_test_call().await; + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(ðereum_fallback_provider).await; assert_eq!(provider_call_count, vec![1, 0, 0]); } @@ -233,21 +229,18 @@ mod tests { async fn test_one_stalled_provider() { let fallback_provider_builder = FallbackProviderBuilder::default(); let providers = vec![ - ProviderMock::new(), - ProviderMock::new(), - ProviderMock::new(), + EthereumProviderMock::new(Some(Duration::from_millis(10))), + EthereumProviderMock::default(), + EthereumProviderMock::default(), ]; let fallback_provider = fallback_provider_builder .add_providers(providers) .with_max_block_time(Duration::from_secs(0)) .build(); let ethereum_fallback_provider = EthereumFallbackProvider::new(fallback_provider); - ethereum_fallback_provider - .request::<_, u64>(BLOCK_NUMBER_RPC, ()) - .await - .unwrap(); - - let provider_call_count: Vec<_> = get_call_counts(ðereum_fallback_provider).await; + ethereum_fallback_provider.low_level_test_call().await; + let provider_call_count: Vec<_> = + ProviderMock::get_call_counts(ðereum_fallback_provider).await; assert_eq!(provider_call_count, vec![0, 0, 2]); } diff --git a/rust/chains/hyperlane-ethereum/src/trait_builder.rs b/rust/chains/hyperlane-ethereum/src/trait_builder.rs index 31fa128d86..a0ab93af48 100644 --- a/rust/chains/hyperlane-ethereum/src/trait_builder.rs +++ b/rust/chains/hyperlane-ethereum/src/trait_builder.rs @@ -16,8 +16,8 @@ use reqwest::{Client, Url}; use thiserror::Error; use ethers_prometheus::json_rpc_client::{ - JsonRpcClientMetrics, JsonRpcClientMetricsBuilder, NodeInfo, PrometheusJsonRpcClient, - PrometheusJsonRpcClientConfig, + JsonRpcBlockGetter, JsonRpcClientMetrics, JsonRpcClientMetricsBuilder, NodeInfo, + PrometheusJsonRpcClient, PrometheusJsonRpcClientConfig, }; use ethers_prometheus::middleware::{ MiddlewareMetrics, PrometheusMiddleware, PrometheusMiddlewareConf, @@ -116,7 +116,10 @@ pub trait BuildableWithProvider { builder = builder.add_provider(metrics_provider); } let fallback_provider = builder.build(); - let ethereum_fallback_provider = EthereumFallbackProvider::new(fallback_provider); + let ethereum_fallback_provider = EthereumFallbackProvider::< + _, + JsonRpcBlockGetter>, + >::new(fallback_provider); self.build( ethereum_fallback_provider, locator, diff --git a/rust/chains/hyperlane-fuel/Cargo.toml b/rust/chains/hyperlane-fuel/Cargo.toml index 7dabcdd514..82bdbc782e 100644 --- a/rust/chains/hyperlane-fuel/Cargo.toml +++ b/rust/chains/hyperlane-fuel/Cargo.toml @@ -19,7 +19,7 @@ tracing-futures.workspace = true tracing.workspace = true url.workspace = true -hyperlane-core = { path = "../../hyperlane-core" } +hyperlane-core = { path = "../../hyperlane-core", features = ["fallback-provider"]} [build-dependencies] abigen = { path = "../../utils/abigen", features = ["fuels"] } diff --git a/rust/chains/hyperlane-sealevel/Cargo.toml b/rust/chains/hyperlane-sealevel/Cargo.toml index 248e3dfac1..ab4e9b17f6 100644 --- a/rust/chains/hyperlane-sealevel/Cargo.toml +++ b/rust/chains/hyperlane-sealevel/Cargo.toml @@ -24,7 +24,7 @@ tracing.workspace = true url.workspace = true account-utils = { path = "../../sealevel/libraries/account-utils" } -hyperlane-core = { path = "../../hyperlane-core", features = ["solana"] } +hyperlane-core = { path = "../../hyperlane-core", features = ["solana", "fallback-provider"] } hyperlane-sealevel-interchain-security-module-interface = { path = "../../sealevel/libraries/interchain-security-module-interface" } hyperlane-sealevel-mailbox = { path = "../../sealevel/programs/mailbox", features = ["no-entrypoint"] } hyperlane-sealevel-igp = { path = "../../sealevel/programs/hyperlane-sealevel-igp", features = ["no-entrypoint"] } diff --git a/rust/config/mainnet3_config.json b/rust/config/mainnet3_config.json index 65e0e8ebd7..522196bf79 100644 --- a/rust/config/mainnet3_config.json +++ b/rust/config/mainnet3_config.json @@ -516,7 +516,11 @@ "http": "https://rpc-injective.goldenratiostaking.net:443" } ], - "grpcUrl": "https://injective-grpc.publicnode.com/", + "grpcUrls": [ + { + "http": "https://injective-grpc.goldenratiostaking.net:443" + } + ], "canonicalAsset": "inj", "bech32Prefix": "inj", "gasPrice": { @@ -896,4 +900,4 @@ } }, "defaultRpcConsensusType": "fallback" -} +} \ No newline at end of file diff --git a/rust/ethers-prometheus/src/json_rpc_client.rs b/rust/ethers-prometheus/src/json_rpc_client.rs index 7e0c4d1fee..2cc8defe9b 100644 --- a/rust/ethers-prometheus/src/json_rpc_client.rs +++ b/rust/ethers-prometheus/src/json_rpc_client.rs @@ -186,9 +186,11 @@ where } } -impl From> for Box { +impl From> + for JsonRpcBlockGetter> +{ fn from(val: PrometheusJsonRpcClient) -> Self { - Box::new(JsonRpcBlockGetter::new(val)) + JsonRpcBlockGetter::new(val) } } diff --git a/rust/helm/hyperlane-agent/templates/external-secret.yaml b/rust/helm/hyperlane-agent/templates/external-secret.yaml index 5d0eae5ced..36f287eca3 100644 --- a/rust/helm/hyperlane-agent/templates/external-secret.yaml +++ b/rust/helm/hyperlane-agent/templates/external-secret.yaml @@ -29,7 +29,7 @@ spec: {{- if not .disabled }} HYP_CHAINS_{{ .name | upper }}_CUSTOMRPCURLS: {{ printf "'{{ .%s_rpcs | mustFromJson | join \",\" }}'" .name }} {{- if eq .protocol "cosmos" }} - HYP_CHAINS_{{ .name | upper }}_GRPCURL: {{ printf "'{{ .%s_grpc }}'" .name }} + HYP_CHAINS_{{ .name | upper }}_GRPCURLS: {{ printf "'{{ .%s_grpcs | mustFromJson | join \",\" }}'" .name }} {{- end }} {{- end }} {{- end }} @@ -44,9 +44,9 @@ spec: remoteRef: key: {{ printf "%s-rpc-endpoints-%s" $.Values.hyperlane.runEnv .name }} {{- if eq .protocol "cosmos" }} - - secretKey: {{ printf "%s_grpc" .name }} + - secretKey: {{ printf "%s_grpcs" .name }} remoteRef: - key: {{ printf "%s-grpc-endpoint-%s" $.Values.hyperlane.runEnv .name }} + key: {{ printf "%s-grpc-endpoints-%s" $.Values.hyperlane.runEnv .name }} {{- end }} {{- end }} {{- end }} diff --git a/rust/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/hyperlane-base/src/settings/parser/connection_parser.rs index 5d42ce4411..0d47d6eca9 100644 --- a/rust/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/hyperlane-base/src/settings/parser/connection_parser.rs @@ -6,7 +6,7 @@ use url::Url; use crate::settings::envs::*; use crate::settings::ChainConnectionConf; -use super::{parse_cosmos_gas_price, ValueParser}; +use super::{parse_base_and_override_urls, parse_cosmos_gas_price, ValueParser}; pub fn build_ethereum_connection_conf( rpcs: &[Url], @@ -43,19 +43,8 @@ pub fn build_cosmos_connection_conf( err: &mut ConfigParsingError, ) -> Option { let mut local_err = ConfigParsingError::default(); - - let grpc_url = chain - .chain(&mut local_err) - .get_key("grpcUrl") - .parse_string() - .end() - .or_else(|| { - local_err.push( - &chain.cwp + "grpc_url", - eyre!("Missing grpc definitions for chain"), - ); - None - }); + let grpcs = + parse_base_and_override_urls(chain, "grpcUrls", "customGrpcUrls", "http", &mut local_err); let chain_id = chain .chain(&mut local_err) @@ -114,7 +103,7 @@ pub fn build_cosmos_connection_conf( None } else { Some(ChainConnectionConf::Cosmos(h_cosmos::ConnectionConf::new( - grpc_url.unwrap().to_string(), + grpcs, rpcs.first().unwrap().to_string(), chain_id.unwrap().to_string(), prefix.unwrap().to_string(), diff --git a/rust/hyperlane-base/src/settings/parser/mod.rs b/rust/hyperlane-base/src/settings/parser/mod.rs index 76c88fe1b3..3bfb52f91f 100644 --- a/rust/hyperlane-base/src/settings/parser/mod.rs +++ b/rust/hyperlane-base/src/settings/parser/mod.rs @@ -18,6 +18,7 @@ use hyperlane_core::{ use itertools::Itertools; use serde::Deserialize; use serde_json::Value; +use url::Url; pub use self::json_value_parser::ValueParser; pub use super::envs::*; @@ -134,47 +135,7 @@ fn parse_chain( .parse_u32() .unwrap_or(1); - let rpcs_base = chain - .chain(&mut err) - .get_key("rpcUrls") - .into_array_iter() - .map(|urls| { - urls.filter_map(|v| { - v.chain(&mut err) - .get_key("http") - .parse_from_str("Invalid http url") - .end() - }) - .collect_vec() - }) - .unwrap_or_default(); - - let rpc_overrides = chain - .chain(&mut err) - .get_opt_key("customRpcUrls") - .parse_string() - .end() - .map(|urls| { - urls.split(',') - .filter_map(|url| { - url.parse() - .take_err(&mut err, || &chain.cwp + "customRpcUrls") - }) - .collect_vec() - }); - - let rpcs = rpc_overrides.unwrap_or(rpcs_base); - - if rpcs.is_empty() { - err.push( - &chain.cwp + "rpc_urls", - eyre!("Missing base rpc definitions for chain"), - ); - err.push( - &chain.cwp + "custom_rpc_urls", - eyre!("Also missing rpc overrides for chain"), - ); - } + let rpcs = parse_base_and_override_urls(&chain, "rpcUrls", "customRpcUrls", "http", &mut err); let from = chain .chain(&mut err) @@ -418,3 +379,66 @@ fn parse_cosmos_gas_price(gas_price: ValueParser) -> ConfigResult Vec { + chain + .chain(err) + .get_key(key) + .into_array_iter() + .map(|urls| { + urls.filter_map(|v| { + v.chain(err) + .get_key(protocol) + .parse_from_str("Invalid url") + .end() + }) + .collect_vec() + }) + .unwrap_or_default() +} + +fn parse_custom_urls( + chain: &ValueParser, + key: &str, + err: &mut ConfigParsingError, +) -> Option> { + chain + .chain(err) + .get_opt_key(key) + .parse_string() + .end() + .map(|urls| { + urls.split(',') + .filter_map(|url| url.parse().take_err(err, || &chain.cwp + "customGrpcUrls")) + .collect_vec() + }) +} + +fn parse_base_and_override_urls( + chain: &ValueParser, + base_key: &str, + override_key: &str, + protocol: &str, + err: &mut ConfigParsingError, +) -> Vec { + let base = parse_urls(chain, base_key, protocol, err); + let overrides = parse_custom_urls(chain, override_key, err); + let combined = overrides.unwrap_or(base); + + if combined.is_empty() { + err.push( + &chain.cwp + "rpc_urls", + eyre!("Missing base rpc definitions for chain"), + ); + err.push( + &chain.cwp + "custom_rpc_urls", + eyre!("Also missing rpc overrides for chain"), + ); + } + combined +} diff --git a/rust/hyperlane-core/Cargo.toml b/rust/hyperlane-core/Cargo.toml index 8329bce5f2..40468bef6c 100644 --- a/rust/hyperlane-core/Cargo.toml +++ b/rust/hyperlane-core/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { workspace = true } sha3 = { workspace = true } strum = { workspace = true, optional = true, features = ["derive"] } thiserror = { workspace = true } +tokio = { workspace = true, optional = true, features = ["rt", "time"] } tracing.workspace = true primitive-types = { workspace = true, optional = true } solana-sdk = { workspace = true, optional = true } @@ -54,3 +55,4 @@ agent = ["ethers", "strum"] strum = ["dep:strum"] ethers = ["dep:ethers-core", "dep:ethers-contract", "dep:ethers-providers", "dep:primitive-types"] solana = ["dep:solana-sdk"] +fallback-provider = ["tokio"] diff --git a/rust/hyperlane-core/src/rpc_clients/fallback.rs b/rust/hyperlane-core/src/rpc_clients/fallback.rs index 6adcb76f55..6f75cbc4c8 100644 --- a/rust/hyperlane-core/src/rpc_clients/fallback.rs +++ b/rust/hyperlane-core/src/rpc_clients/fallback.rs @@ -1,15 +1,22 @@ use async_rwlock::RwLock; use async_trait::async_trait; use derive_new::new; +use itertools::Itertools; use std::{ - fmt::Debug, + fmt::{Debug, Formatter}, + future::Future, + marker::PhantomData, + pin::Pin, sync::Arc, time::{Duration, Instant}, }; -use tracing::info; +use tokio; +use tracing::{info, trace, warn_span}; use crate::ChainCommunicationError; +use super::RpcClientError; + /// Read the current block number from a chain. #[async_trait] pub trait BlockNumberGetter: Send + Sync + Debug { @@ -38,7 +45,6 @@ impl PrioritizedProviderInner { } } } - /// Sub-providers and priority information pub struct PrioritizedProviders { /// Unsorted list of providers this provider calls @@ -49,28 +55,56 @@ pub struct PrioritizedProviders { /// A provider that bundles multiple providers and attempts to call the first, /// then the second, and so on until a response is received. -pub struct FallbackProvider { +/// +/// Although no trait bounds are used in the struct definition, the intended purpose of `B` +/// is to be bound by `BlockNumberGetter` and have `T` be convertible to `B`. That is, +/// inner providers should be able to get the current block number, or be convertible into +/// something that is. +pub struct FallbackProvider { /// The sub-providers called by this provider pub inner: Arc>, max_block_time: Duration, + _phantom: PhantomData, } -impl Clone for FallbackProvider { +impl Clone for FallbackProvider { fn clone(&self) -> Self { Self { inner: self.inner.clone(), max_block_time: self.max_block_time, + _phantom: PhantomData, } } } -impl FallbackProvider +impl Debug for FallbackProvider +where + T: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // iterate the inner providers and write them to the formatter + f.debug_struct("FallbackProvider") + .field( + "providers", + &self + .inner + .providers + .iter() + .map(|v| format!("{:?}", v)) + .join(", "), + ) + .finish() + } +} + +impl FallbackProvider where - T: Into> + Debug + Clone, + T: Into + Debug + Clone, + B: BlockNumberGetter, { /// Convenience method for creating a `FallbackProviderBuilder` with same /// `JsonRpcClient` types - pub fn builder() -> FallbackProviderBuilder { + pub fn builder() -> FallbackProviderBuilder { FallbackProviderBuilder::default() } @@ -112,7 +146,7 @@ where return; } - let block_getter: Box = provider.clone().into(); + let block_getter: B = provider.clone().into(); let current_block_height = block_getter .get_block_number() .await @@ -130,25 +164,62 @@ where .await; } } + + /// Call the first provider, then the second, and so on (in order of priority) until a response is received. + /// If all providers fail, return an error. + pub async fn call( + &self, + mut f: impl FnMut(T) -> Pin> + Send>>, + ) -> Result { + let mut errors = vec![]; + // make sure we do at least 4 total retries. + while errors.len() <= 3 { + if !errors.is_empty() { + tokio::time::sleep(Duration::from_millis(100)).await; + } + let priorities_snapshot = self.take_priorities_snapshot().await; + for (idx, priority) in priorities_snapshot.iter().enumerate() { + let provider = &self.inner.providers[priority.index]; + let resp = f(provider.clone()).await; + self.handle_stalled_provider(priority, provider).await; + let _span = + warn_span!("FallbackProvider::call", fallback_count=%idx, provider_index=%priority.index, ?provider).entered(); + match resp { + Ok(v) => return Ok(v), + Err(e) => { + trace!( + error=?e, + "Got error from inner fallback provider", + ); + errors.push(e) + } + } + } + } + + Err(RpcClientError::FallbackProvidersFailed(errors).into()) + } } /// Builder to create a new fallback provider. #[derive(Debug, Clone)] -pub struct FallbackProviderBuilder { +pub struct FallbackProviderBuilder { providers: Vec, max_block_time: Duration, + _phantom: PhantomData, } -impl Default for FallbackProviderBuilder { +impl Default for FallbackProviderBuilder { fn default() -> Self { Self { providers: Vec::new(), max_block_time: MAX_BLOCK_TIME, + _phantom: PhantomData, } } } -impl FallbackProviderBuilder { +impl FallbackProviderBuilder { /// Add a new provider to the set. Each new provider will be a lower /// priority than the previous. pub fn add_provider(mut self, provider: T) -> Self { @@ -170,7 +241,7 @@ impl FallbackProviderBuilder { } /// Create a fallback provider. - pub fn build(self) -> FallbackProvider { + pub fn build(self) -> FallbackProvider { let provider_count = self.providers.len(); let prioritized_providers = PrioritizedProviders { providers: self.providers, @@ -184,6 +255,80 @@ impl FallbackProviderBuilder { FallbackProvider { inner: Arc::new(prioritized_providers), max_block_time: self.max_block_time, + _phantom: PhantomData, + } + } +} + +/// Utilities to import when testing chain-specific fallback providers +pub mod test { + use super::*; + use std::{ + ops::Deref, + sync::{Arc, Mutex}, + }; + + /// Provider that stores requests and optionally sleeps before returning a dummy value + #[derive(Debug, Clone)] + pub struct ProviderMock { + // Store requests as tuples of (method, params) + // Even if the tests were single-threaded, need the arc-mutex + // for interior mutability in `JsonRpcClient::request` + requests: Arc>>, + request_sleep: Option, + } + + impl Default for ProviderMock { + fn default() -> Self { + Self { + requests: Arc::new(Mutex::new(vec![])), + request_sleep: None, + } + } + } + + impl ProviderMock { + /// Create a new provider + pub fn new(request_sleep: Option) -> Self { + Self { + request_sleep, + ..Default::default() + } + } + + /// Push a request to the internal store for later inspection + pub fn push(&self, method: &str, params: T) { + self.requests + .lock() + .unwrap() + .push((method.to_owned(), format!("{:?}", params))); + } + + /// Get the stored requests + pub fn requests(&self) -> Vec<(String, String)> { + self.requests.lock().unwrap().clone() + } + + /// Set the sleep duration + pub fn request_sleep(&self) -> Option { + self.request_sleep + } + + /// Get how many times each provider was called + pub async fn get_call_counts, B>( + fallback_provider: &FallbackProvider, + ) -> Vec { + fallback_provider + .inner + .priorities + .read() + .await + .iter() + .map(|p| { + let provider = &fallback_provider.inner.providers[p.index]; + provider.requests().len() + }) + .collect() } } } diff --git a/rust/hyperlane-core/src/rpc_clients/mod.rs b/rust/hyperlane-core/src/rpc_clients/mod.rs index 78851f9f26..02aaae99f5 100644 --- a/rust/hyperlane-core/src/rpc_clients/mod.rs +++ b/rust/hyperlane-core/src/rpc_clients/mod.rs @@ -1,4 +1,8 @@ -pub use self::{error::*, fallback::*}; +pub use self::error::*; + +#[cfg(feature = "fallback-provider")] +pub use self::fallback::*; mod error; +#[cfg(feature = "fallback-provider")] mod fallback; diff --git a/rust/utils/abigen/src/lib.rs b/rust/utils/abigen/src/lib.rs index 2e8d5ca081..b4b7970fdc 100644 --- a/rust/utils/abigen/src/lib.rs +++ b/rust/utils/abigen/src/lib.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "fuels")] use fuels_code_gen::ProgramType; use std::collections::BTreeSet; use std::ffi::OsStr; diff --git a/rust/utils/run-locally/src/cosmos/types.rs b/rust/utils/run-locally/src/cosmos/types.rs index 795d12ff39..ed95331cea 100644 --- a/rust/utils/run-locally/src/cosmos/types.rs +++ b/rust/utils/run-locally/src/cosmos/types.rs @@ -118,7 +118,7 @@ pub struct AgentConfig { pub protocol: String, pub chain_id: String, pub rpc_urls: Vec, - pub grpc_url: String, + pub grpc_urls: Vec, pub bech32_prefix: String, pub signer: AgentConfigSigner, pub index: AgentConfigIndex, @@ -156,7 +156,15 @@ impl AgentConfig { network.launch_resp.endpoint.rpc_addr.replace("tcp://", "") ), }], - grpc_url: format!("http://{}", network.launch_resp.endpoint.grpc_addr), + grpc_urls: vec![ + // The first url points to a nonexistent node, but is used for checking fallback provider logic + AgentUrl { + http: "localhost:1337".to_string(), + }, + AgentUrl { + http: format!("http://{}", network.launch_resp.endpoint.grpc_addr), + }, + ], bech32_prefix: "osmo".to_string(), signer: AgentConfigSigner { typ: "cosmosKey".to_string(), diff --git a/typescript/sdk/src/metadata/agentConfig.ts b/typescript/sdk/src/metadata/agentConfig.ts index e7133b4c4a..5aa89687ff 100644 --- a/typescript/sdk/src/metadata/agentConfig.ts +++ b/typescript/sdk/src/metadata/agentConfig.ts @@ -124,7 +124,7 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge( .string() .optional() .describe( - 'Specify a comma seperated list of custom RPC URLs to use for this chain. If not specified, the default RPC urls will be used.', + 'Specify a comma separated list of custom RPC URLs to use for this chain. If not specified, the default RPC urls will be used.', ), rpcConsensusType: z .nativeEnum(RpcConsensusType) diff --git a/typescript/sdk/src/metadata/chainMetadataTypes.ts b/typescript/sdk/src/metadata/chainMetadataTypes.ts index b13cea0349..565a151af0 100644 --- a/typescript/sdk/src/metadata/chainMetadataTypes.ts +++ b/typescript/sdk/src/metadata/chainMetadataTypes.ts @@ -115,6 +115,12 @@ export const ChainMetadataSchemaObject = z.object({ .array(RpcUrlSchema) .describe('For cosmos chains only, a list of gRPC API URLs') .optional(), + customGrpcUrls: z + .string() + .optional() + .describe( + 'Specify a comma separated list of custom GRPC URLs to use for this chain. If not specified, the default GRPC urls will be used.', + ), blockExplorers: z .array( z.object({ From 634873fe31fef2f226ea9bc1c32e2d0fa6faffea Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 5 Feb 2024 17:39:01 -0500 Subject: [PATCH 8/9] Rotate chain metadata RPC urls (#3229) ### Description - existing viction and sepolia RPC urls were failing on eth_getCode ### Testing - CI fork tests --- typescript/sdk/src/consts/chainMetadata.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typescript/sdk/src/consts/chainMetadata.ts b/typescript/sdk/src/consts/chainMetadata.ts index 7a8a469b66..7e9b91b76c 100644 --- a/typescript/sdk/src/consts/chainMetadata.ts +++ b/typescript/sdk/src/consts/chainMetadata.ts @@ -853,6 +853,7 @@ export const sepolia: ChainMetadata = { nativeToken: etherToken, protocol: ProtocolType.Ethereum, rpcUrls: [ + { http: 'https://ethereum-sepolia.publicnode.com' }, { http: 'https://ethereum-sepolia.blockpi.network/v1/rpc/public' }, { http: 'https://rpc.sepolia.org' }, ], @@ -1036,6 +1037,9 @@ export const viction: ChainMetadata = { }, protocol: ProtocolType.Ethereum, rpcUrls: [ + { + http: 'https://rpc.tomochain.com/', + }, { http: 'https://viction.blockpi.network/v1/rpc/public', }, From 0ff3e2b1713d38e983a8af805a9baa52aec811d2 Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Mon, 5 Feb 2024 18:47:00 -0500 Subject: [PATCH 9/9] Deploy injective and inEVM (#3140) ### Description - Deploy core to injective and inEVM networks ### Drive-by changes - Update cw-hyperlane dependency ### Backward compatibility Yes ### Testing - Fork tests - e2e tests --------- Co-authored-by: J M Rossy Co-authored-by: Trevor Porter Co-authored-by: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Co-authored-by: Nam Chu Hoai --- .github/workflows/test.yml | 7 +- rust/Cargo.lock | 48 +++++----- rust/Cargo.toml | 2 +- rust/README.md | 6 ++ rust/chains/hyperlane-cosmos/Cargo.toml | 2 +- .../src/interchain_security_module.rs | 2 +- .../src/payloads/ism_routes.rs | 2 +- rust/chains/hyperlane-cosmos/src/types.rs | 28 +++--- .../testwarproute/program-ids.json | 8 +- rust/utils/run-locally/Cargo.toml | 2 +- rust/utils/run-locally/src/cosmos/crypto.rs | 2 +- rust/utils/run-locally/src/cosmos/deploy.rs | 45 ++++----- rust/utils/run-locally/src/cosmos/link.rs | 36 ++------ rust/utils/run-locally/src/cosmos/mod.rs | 25 ++--- rust/utils/run-locally/src/cosmos/types.rs | 3 +- .../config/environments/mainnet3/chains.ts | 7 +- .../mainnet3/core/verification.json | 92 +++++++++++++++++++ .../environments/mainnet3/gas-oracle.ts | 5 + .../infra/config/environments/mainnet3/igp.ts | 22 +++-- .../mainnet3/ism/verification.json | 62 +++++++++++++ .../config/environments/mainnet3/owners.ts | 27 +++--- .../environments/mainnet3/validators.ts | 32 +++++++ .../environments/mainnet3/warp/addresses.json | 4 +- .../environments/testnet4/validators.ts | 12 +++ typescript/infra/scripts/utils.ts | 17 +++- typescript/sdk/src/consts/chainMetadata.ts | 54 +++++++++++ typescript/sdk/src/consts/chains.ts | 4 + .../sdk/src/consts/environments/mainnet.json | 20 ++++ typescript/sdk/src/consts/multisigIsm.ts | 18 ++++ 29 files changed, 455 insertions(+), 139 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75d1e5bfb7..47a2b8f49a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -244,13 +244,16 @@ jobs: needs: [yarn-build] strategy: matrix: - environment: [testnet4, mainnet3] - module: [core, igp, helloworld] include: - environment: testnet4 chain: sepolia + module: core - environment: mainnet3 chain: arbitrum + module: core + - environment: mainnet3 + chain: inevm + module: core - environment: mainnet3 chain: viction module: core diff --git a/rust/Cargo.lock b/rust/Cargo.lock index ae5e616edf..55917f5255 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3958,28 +3958,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hpl-interface" -version = "0.0.6-rc3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9124bd88d88aba044ae790007bcbf25493b93424156e8759523c9c5d02229b0c" -dependencies = [ - "bech32 0.9.1", - "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", - "cw-storage-plus", - "cw2", - "cw20", - "cw20-base", - "ripemd", - "schemars", - "serde", - "sha2 0.10.8", - "sha3 0.10.8", - "thiserror", -] - [[package]] name = "http" version = "0.2.11" @@ -4219,11 +4197,11 @@ dependencies = [ "cosmrs", "derive-new", "hex 0.4.3", - "hpl-interface", "http", "hyper", "hyper-tls", "hyperlane-core", + "hyperlane-cosmwasm-interface", "injective-protobuf", "injective-std", "itertools 0.12.0", @@ -4244,6 +4222,28 @@ dependencies = [ "url", ] +[[package]] +name = "hyperlane-cosmwasm-interface" +version = "0.0.6-rc6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e622014ab94f1e7f0acbe71df7c1384224261e2c76115807aaf24215970942" +dependencies = [ + "bech32 0.9.1", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "cw2", + "cw20", + "cw20-base", + "ripemd", + "schemars", + "serde", + "sha2 0.10.8", + "sha3 0.10.8", + "thiserror", +] + [[package]] name = "hyperlane-ethereum" version = "0.1.0" @@ -7141,9 +7141,9 @@ dependencies = [ "ctrlc", "eyre", "hex 0.4.3", - "hpl-interface", "hyperlane-core", "hyperlane-cosmos", + "hyperlane-cosmwasm-interface", "jobserver", "k256 0.13.2", "macro_rules_attribute", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 1c9a2e2ff4..c1a1feb008 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -91,10 +91,10 @@ bech32 = "0.9.1" elliptic-curve = "0.12.3" getrandom = { version = "0.2", features = ["js"] } hex = "0.4.3" -hpl-interface = "=0.0.6-rc3" http = "*" hyper = "0.14" hyper-tls = "0.5.0" +hyperlane-cosmwasm-interface = "=0.0.6-rc6" injective-protobuf = "0.2.2" injective-std = "0.1.5" itertools = "*" diff --git a/rust/README.md b/rust/README.md index 51bba96136..309d507c47 100644 --- a/rust/README.md +++ b/rust/README.md @@ -94,6 +94,12 @@ cargo run --release --bin run-locally This will automatically build the agents, start a local node, build and deploy the contracts, and run a relayer and validator. By default, this test will run indefinitely, but can be stopped with `ctrl-c`. +To run the tests for a specific VM, use the `--features` flag. + +```bash +cargo test --release --package run-locally --bin run-locally --features cosmos -- cosmos::test --nocapture +``` + ### Building Agent Docker Images There exists a docker build for the agent binaries. These docker images are used for deploying the agents in a diff --git a/rust/chains/hyperlane-cosmos/Cargo.toml b/rust/chains/hyperlane-cosmos/Cargo.toml index 6115a61e6d..c105faec66 100644 --- a/rust/chains/hyperlane-cosmos/Cargo.toml +++ b/rust/chains/hyperlane-cosmos/Cargo.toml @@ -16,8 +16,8 @@ bech32 = { workspace = true } cosmrs = { workspace = true, features = ["cosmwasm", "tokio", "grpc", "rpc"] } derive-new = { workspace = true } hex = { workspace = true } -hpl-interface.workspace = true http = { workspace = true } +hyperlane-cosmwasm-interface.workspace = true hyper = { workspace = true } hyper-tls = { workspace = true } injective-protobuf = { workspace = true } diff --git a/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs b/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs index bfde29f298..dd495be89a 100644 --- a/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs +++ b/rust/chains/hyperlane-cosmos/src/interchain_security_module.rs @@ -82,7 +82,7 @@ impl InterchainSecurityModule for CosmosInterchainSecurityModule { .await?; let module_type_response = - serde_json::from_slice::(&data)?; + serde_json::from_slice::(&data)?; Ok(IsmType(module_type_response.typ).into()) } diff --git a/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs b/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs index 1f659840e4..4a0563945f 100644 --- a/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs +++ b/rust/chains/hyperlane-cosmos/src/payloads/ism_routes.rs @@ -39,5 +39,5 @@ pub struct QueryIsmModuleTypeRequest { #[derive(Serialize, Deserialize, Debug)] pub struct QueryIsmModuleTypeResponse { #[serde(rename = "type")] - pub typ: hpl_interface::ism::IsmType, + pub typ: hyperlane_cosmwasm_interface::ism::IsmType, } diff --git a/rust/chains/hyperlane-cosmos/src/types.rs b/rust/chains/hyperlane-cosmos/src/types.rs index d7647e7ddf..aa5a954650 100644 --- a/rust/chains/hyperlane-cosmos/src/types.rs +++ b/rust/chains/hyperlane-cosmos/src/types.rs @@ -1,10 +1,10 @@ use cosmrs::proto::cosmos::base::abci::v1beta1::TxResponse; use hyperlane_core::{ChainResult, ModuleType, TxOutcome, H256, U256}; -pub struct IsmType(pub hpl_interface::ism::IsmType); +pub struct IsmType(pub hyperlane_cosmwasm_interface::ism::IsmType); -impl From for IsmType { - fn from(value: hpl_interface::ism::IsmType) -> Self { +impl From for IsmType { + fn from(value: hyperlane_cosmwasm_interface::ism::IsmType) -> Self { IsmType(value) } } @@ -12,14 +12,20 @@ impl From for IsmType { impl From for ModuleType { fn from(value: IsmType) -> Self { match value.0 { - hpl_interface::ism::IsmType::Unused => ModuleType::Unused, - hpl_interface::ism::IsmType::Routing => ModuleType::Routing, - hpl_interface::ism::IsmType::Aggregation => ModuleType::Aggregation, - hpl_interface::ism::IsmType::LegacyMultisig => ModuleType::MessageIdMultisig, - hpl_interface::ism::IsmType::MerkleRootMultisig => ModuleType::MerkleRootMultisig, - hpl_interface::ism::IsmType::MessageIdMultisig => ModuleType::MessageIdMultisig, - hpl_interface::ism::IsmType::Null => ModuleType::Null, - hpl_interface::ism::IsmType::CcipRead => ModuleType::CcipRead, + hyperlane_cosmwasm_interface::ism::IsmType::Unused => ModuleType::Unused, + hyperlane_cosmwasm_interface::ism::IsmType::Routing => ModuleType::Routing, + hyperlane_cosmwasm_interface::ism::IsmType::Aggregation => ModuleType::Aggregation, + hyperlane_cosmwasm_interface::ism::IsmType::LegacyMultisig => { + ModuleType::MessageIdMultisig + } + hyperlane_cosmwasm_interface::ism::IsmType::MerkleRootMultisig => { + ModuleType::MerkleRootMultisig + } + hyperlane_cosmwasm_interface::ism::IsmType::MessageIdMultisig => { + ModuleType::MessageIdMultisig + } + hyperlane_cosmwasm_interface::ism::IsmType::Null => ModuleType::Null, + hyperlane_cosmwasm_interface::ism::IsmType::CcipRead => ModuleType::CcipRead, } } } diff --git a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json index ba62748efe..c5e945eae3 100644 --- a/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json +++ b/rust/sealevel/environments/local-e2e/warp-routes/testwarproute/program-ids.json @@ -1,10 +1,10 @@ { - "sealeveltest1": { - "hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b", - "base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" - }, "sealeveltest2": { "hex": "0x2317f9615d4ebc2419ad4b88580e2a80a03b2c7a60bc960de7d6934dbc37a87e", "base58": "3MzUPjP5LEkiHH82nEAe28Xtz9ztuMqWc8UmuKxrpVQH" + }, + "sealeveltest1": { + "hex": "0xa77b4e2ed231894cc8cb8eee21adcc705d8489bccc6b2fcf40a358de23e60b7b", + "base58": "CGn8yNtSD3aTTqJfYhUb6s1aVTN75NzwtsFKo1e83aga" } } \ No newline at end of file diff --git a/rust/utils/run-locally/Cargo.toml b/rust/utils/run-locally/Cargo.toml index 03d771734e..bf57138f4e 100644 --- a/rust/utils/run-locally/Cargo.toml +++ b/rust/utils/run-locally/Cargo.toml @@ -29,7 +29,7 @@ ureq = { workspace = true, default-features = false } which.workspace = true macro_rules_attribute.workspace = true regex.workspace = true -hpl-interface.workspace = true +hyperlane-cosmwasm-interface.workspace = true cosmwasm-schema.workspace = true [features] diff --git a/rust/utils/run-locally/src/cosmos/crypto.rs b/rust/utils/run-locally/src/cosmos/crypto.rs index 9b336f4df7..75924df69a 100644 --- a/rust/utils/run-locally/src/cosmos/crypto.rs +++ b/rust/utils/run-locally/src/cosmos/crypto.rs @@ -24,7 +24,7 @@ pub fn pub_to_addr(pub_key: &[u8], prefix: &str) -> String { let sha_hash = sha256_digest(pub_key); let rip_hash = ripemd160_digest(sha_hash); - let addr = hpl_interface::types::bech32_encode(prefix, &rip_hash).unwrap(); + let addr = hyperlane_cosmwasm_interface::types::bech32_encode(prefix, &rip_hash).unwrap(); addr.to_string() } diff --git a/rust/utils/run-locally/src/cosmos/deploy.rs b/rust/utils/run-locally/src/cosmos/deploy.rs index ab02a2bee4..4da016d865 100644 --- a/rust/utils/run-locally/src/cosmos/deploy.rs +++ b/rust/utils/run-locally/src/cosmos/deploy.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use hpl_interface::{core, hook, igp, ism}; +use hyperlane_cosmwasm_interface::{core, hook, igp, ism}; use macro_rules_attribute::apply; use crate::utils::as_task; @@ -9,19 +9,18 @@ use super::{ types::{Codes, Deployments}, }; -#[cw_serde] -pub struct IsmMultisigInstantiateMsg { - pub owner: String, -} - #[cw_serde] pub struct TestMockMsgReceiverInstantiateMsg { pub hrp: String, } #[cw_serde] -pub struct IGPOracleInstantiateMsg { +struct IgpInstantiateMsg { + pub hrp: String, pub owner: String, + pub gas_token: String, + pub beneficiary: String, + pub default_gas_usage: String, // u128 doesnt work with cw_serde } #[cw_serde] @@ -52,22 +51,12 @@ pub fn deploy_cw_hyperlane( "hpl_mailbox", ); - // deploy igp set - #[cw_serde] - pub struct GasOracleInitMsg { - pub hrp: String, - pub owner: String, - pub gas_token: String, - pub beneficiary: String, - pub default_gas_usage: String, - } - let igp = cli.wasm_init( &endpoint, &deployer, Some(deployer_addr), codes.hpl_igp, - GasOracleInitMsg { + IgpInstantiateMsg { hrp: BECH32_PREFIX.to_string(), owner: deployer_addr.clone(), gas_token: "uosmo".to_string(), @@ -107,12 +96,25 @@ pub fn deploy_cw_hyperlane( &deployer, Some(deployer_addr), codes.hpl_ism_multisig, - IsmMultisigInstantiateMsg { + ism::multisig::InstantiateMsg { owner: deployer_addr.clone(), }, "hpl_ism_multisig", ); + // deploy pausable ism + let ism_pausable = cli.wasm_init( + &endpoint, + &deployer, + Some(deployer_addr), + codes.hpl_ism_pausable, + ism::pausable::InstantiateMsg { + owner: deployer_addr.clone(), + paused: false, + }, + "hpl_ism_pausable", + ); + // deploy ism - aggregation let ism_aggregate = cli.wasm_init( &endpoint, @@ -121,8 +123,8 @@ pub fn deploy_cw_hyperlane( codes.hpl_ism_aggregate, ism::aggregate::InstantiateMsg { owner: deployer_addr.clone(), - threshold: 1, - isms: vec![ism_multisig.clone()], + threshold: 2, + isms: vec![ism_multisig.clone(), ism_pausable.clone()], }, "hpl_ism_aggregate", ); @@ -134,7 +136,6 @@ pub fn deploy_cw_hyperlane( Some(deployer_addr), codes.hpl_hook_merkle, hook::merkle::InstantiateMsg { - owner: deployer_addr.clone(), mailbox: mailbox.to_string(), }, "hpl_hook_merkle", diff --git a/rust/utils/run-locally/src/cosmos/link.rs b/rust/utils/run-locally/src/cosmos/link.rs index ff3de059fa..ebf53bd36c 100644 --- a/rust/utils/run-locally/src/cosmos/link.rs +++ b/rust/utils/run-locally/src/cosmos/link.rs @@ -1,7 +1,7 @@ use std::path::Path; use cosmwasm_schema::cw_serde; -use hpl_interface::{ +use hyperlane_cosmwasm_interface::{ core, ism::{self}, }; @@ -69,18 +69,14 @@ pub struct MockQuoteDispatch { #[cw_serde] pub struct GeneralIsmValidatorMessage { - pub enroll_validator: EnrollValidatorMsg, + pub set_validators: SetValidatorsMsg, } #[cw_serde] -pub struct EnrollValidatorMsg { - pub set: EnrollValidatorSet, -} - -#[cw_serde] -pub struct EnrollValidatorSet { +pub struct SetValidatorsMsg { pub domain: u32, - pub validator: String, + pub threshold: u8, + pub validators: Vec, } fn link_network( @@ -105,7 +101,7 @@ fn link_network( let public_key = validator.priv_key.verifying_key().to_encoded_point(false); let public_key = public_key.as_bytes(); - let hash = hpl_interface::types::keccak256_hash(&public_key[1..]); + let hash = hyperlane_cosmwasm_interface::types::keccak256_hash(&public_key[1..]); let mut bytes = [0u8; 20]; bytes.copy_from_slice(&hash.as_slice()[12..]); @@ -115,24 +111,10 @@ fn link_network( linker, &network.deployments.ism_multisig, GeneralIsmValidatorMessage { - enroll_validator: EnrollValidatorMsg { - set: EnrollValidatorSet { - domain: target_domain, - validator: hex::encode(bytes).to_string(), - }, - }, - }, - vec![], - ); - - cli.wasm_execute( - &network.launch_resp.endpoint, - linker, - &network.deployments.ism_multisig, - ism::multisig::ExecuteMsg::SetThreshold { - set: ism::multisig::ThresholdSet { - domain: target_domain, + set_validators: SetValidatorsMsg { threshold: 1, + domain: target_domain, + validators: vec![hex::encode(bytes).to_string()], }, }, vec![], diff --git a/rust/utils/run-locally/src/cosmos/mod.rs b/rust/utils/run-locally/src/cosmos/mod.rs index a61df94166..b1b2b4dd47 100644 --- a/rust/utils/run-locally/src/cosmos/mod.rs +++ b/rust/utils/run-locally/src/cosmos/mod.rs @@ -5,8 +5,8 @@ use std::time::{Duration, Instant}; use std::{env, fs}; use cosmwasm_schema::cw_serde; -use hpl_interface::types::bech32_decode; use hyperlane_cosmos::RawCosmosAmount; +use hyperlane_cosmwasm_interface::types::bech32_decode; use macro_rules_attribute::apply; use maplit::hashmap; use tempfile::tempdir; @@ -57,8 +57,8 @@ fn default_keys<'a>() -> [(&'a str, &'a str); 6] { ] } -const CW_HYPERLANE_GIT: &str = "https://github.com/many-things/cw-hyperlane"; -const CW_HYPERLANE_VERSION: &str = "0.0.6-rc6"; +const CW_HYPERLANE_GIT: &str = "https://github.com/hyperlane-xyz/cosmwasm"; +const CW_HYPERLANE_VERSION: &str = "v0.0.6"; fn make_target() -> String { let os = if cfg!(target_os = "linux") { @@ -101,19 +101,22 @@ pub fn install_codes(dir: Option, local: bool) -> BTreeMap path map fs::read_dir(dir_path) diff --git a/rust/utils/run-locally/src/cosmos/types.rs b/rust/utils/run-locally/src/cosmos/types.rs index ed95331cea..120ed05afd 100644 --- a/rust/utils/run-locally/src/cosmos/types.rs +++ b/rust/utils/run-locally/src/cosmos/types.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, path::PathBuf}; -use hpl_interface::types::bech32_decode; use hyperlane_cosmos::RawCosmosAmount; +use hyperlane_cosmwasm_interface::types::bech32_decode; use super::{cli::OsmosisCLI, CosmosNetwork}; @@ -44,6 +44,7 @@ pub struct Codes { pub hpl_igp_oracle: u64, pub hpl_ism_aggregate: u64, pub hpl_ism_multisig: u64, + pub hpl_ism_pausable: u64, pub hpl_ism_routing: u64, pub hpl_test_mock_ism: u64, pub hpl_test_mock_hook: u64, diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index 2cd00593f8..b919d1a7fe 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -50,13 +50,14 @@ export const ethereumMainnetConfigs: ChainMap = { // Blessed non-Ethereum chains. export const nonEthereumMainnetConfigs: ChainMap = { - solana: chainMetadata.solana, - neutron: chainMetadata.neutron, + // solana: chainMetadata.solana, + // neutron: chainMetadata.neutron, + injective: chainMetadata.injective, }; export const mainnetConfigs: ChainMap = { ...ethereumMainnetConfigs, - // ...nonEthereumMainnetConfigs, + ...nonEthereumMainnetConfigs, }; export type MainnetChains = keyof typeof mainnetConfigs; diff --git a/typescript/infra/config/environments/mainnet3/core/verification.json b/typescript/infra/config/environments/mainnet3/core/verification.json index 7b27ed5bc8..c559233f17 100644 --- a/typescript/infra/config/environments/mainnet3/core/verification.json +++ b/typescript/infra/config/environments/mainnet3/core/verification.json @@ -2048,5 +2048,97 @@ "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", "isProxy": false } + ], + "inevm": [ + { + "name": "ProxyAdmin", + "address": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "Mailbox", + "address": "0x4Ed7d626f1E96cD1C0401607Bf70D95243E3dEd1", + "constructorArguments": "00000000000000000000000000000000000000000000000000000000000009dd", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "constructorArguments": "0000000000000000000000004ed7d626f1e96cd1c0401607bf70d95243e3ded10000000000000000000000000761b0827849abbf7b0cc09ce14e1c93d87f500400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", + "isProxy": true + }, + { + "name": "MerkleTreeHook", + "address": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", + "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x481171eb1aad17eDE6a56005B7F1aB00C581ef13", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x19dc38aeae620380430C200a6E990D5Af5480117", + "constructorArguments": "000000000000000000000000481171eb1aad17ede6a56005b7f1ab00c581ef130000000000000000000000000761b0827849abbf7b0cc09ce14e1c93d87f500400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true + }, + { + "name": "MerkleTreeHook", + "address": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", + "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", + "isProxy": false + }, + { + "name": "StorageGasOracle", + "address": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "InterchainGasPaymaster", + "address": "0x481171eb1aad17eDE6a56005B7F1aB00C581ef13", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "TransparentUpgradeableProxy", + "address": "0x19dc38aeae620380430C200a6E990D5Af5480117", + "constructorArguments": "000000000000000000000000481171eb1aad17ede6a56005b7f1ab00c581ef130000000000000000000000000761b0827849abbf7b0cc09ce14e1c93d87f500400000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000044485cc955000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba00000000000000000000000000000000000000000000000000000000", + "isProxy": true + }, + { + "name": "ProtocolFee", + "address": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", + "constructorArguments": "000000000000000000000000000000000000000000000000000000003b9aca000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba000000000000000000000000a7eccdb9be08178f896c26b7bbd8c3d4e844d9ba", + "isProxy": false + }, + { + "name": "ValidatorAnnounce", + "address": "0x15ab173bDB6832f9b64276bA128659b0eD77730B", + "constructorArguments": "0000000000000000000000002f2afae1139ce54fefc03593fee8ab2adf4a85a7", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "PausableHook", + "address": "0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0", + "constructorArguments": "", + "isProxy": false + } ] } diff --git a/typescript/infra/config/environments/mainnet3/gas-oracle.ts b/typescript/infra/config/environments/mainnet3/gas-oracle.ts index fe5c40ebb6..0775a7abc1 100644 --- a/typescript/infra/config/environments/mainnet3/gas-oracle.ts +++ b/typescript/infra/config/environments/mainnet3/gas-oracle.ts @@ -50,6 +50,8 @@ const gasPrices: ChainMap = { polygonzkevm: ethers.utils.parseUnits('2', 'gwei'), neutron: ethers.utils.parseUnits('1', 'gwei'), mantapacific: ethers.utils.parseUnits('1', 'gwei'), + inevm: ethers.utils.parseUnits('1', 'gwei'), + injective: ethers.utils.parseUnits('1', 'gwei'), viction: ethers.utils.parseUnits('0.25', 'gwei'), }; @@ -93,6 +95,9 @@ const tokenUsdPrices: ChainMap = { '1619.00', TOKEN_EXCHANGE_RATE_DECIMALS, ), + // https://www.coingecko.com/en/coins/injective + injective: ethers.utils.parseUnits('32.78', TOKEN_EXCHANGE_RATE_DECIMALS), + inevm: ethers.utils.parseUnits('32.78', TOKEN_EXCHANGE_RATE_DECIMALS), // 1:1 injective // https://www.coingecko.com/en/coins/viction viction: ethers.utils.parseUnits('0.881', TOKEN_EXCHANGE_RATE_DECIMALS), }; diff --git a/typescript/infra/config/environments/mainnet3/igp.ts b/typescript/infra/config/environments/mainnet3/igp.ts index 19a894d136..d5e999abfa 100644 --- a/typescript/infra/config/environments/mainnet3/igp.ts +++ b/typescript/infra/config/environments/mainnet3/igp.ts @@ -18,6 +18,8 @@ import { owners } from './owners'; const KEY_FUNDER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; const DEPLOYER_ADDRESS = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; +const FOREIGN_DEFAULT_OVERHEAD = 600_000; // cosmwasm warp route somewhat arbitrarily chosen + function getGasOracles(local: MainnetChains) { return Object.fromEntries( exclude(local, supportedChainNames).map((name) => [ @@ -27,19 +29,23 @@ function getGasOracles(local: MainnetChains) { ); } -export const igp: ChainMap = objMap(owners, (chain, owner) => ({ +const remoteOverhead = (remote: MainnetChains) => + ethereumChainNames.includes(remote) + ? multisigIsmVerificationCost( + defaultMultisigConfigs[remote].threshold, + defaultMultisigConfigs[remote].validators.length, + ) + : FOREIGN_DEFAULT_OVERHEAD; // non-ethereum overhead + +export const igp: ChainMap = objMap(owners, (local, owner) => ({ ...owner, oracleKey: DEPLOYER_ADDRESS, beneficiary: KEY_FUNDER_ADDRESS, - gasOracleType: getGasOracles(chain), + gasOracleType: getGasOracles(local), overhead: Object.fromEntries( - // Not setting overhead for non-Ethereum destination chains - exclude(chain, ethereumChainNames).map((remote) => [ + exclude(local, supportedChainNames).map((remote) => [ remote, - multisigIsmVerificationCost( - defaultMultisigConfigs[remote].threshold, - defaultMultisigConfigs[remote].validators.length, - ), + remoteOverhead(remote), ]), ), })); diff --git a/typescript/infra/config/environments/mainnet3/ism/verification.json b/typescript/infra/config/environments/mainnet3/ism/verification.json index e3941bf803..1b8f42b360 100644 --- a/typescript/infra/config/environments/mainnet3/ism/verification.json +++ b/typescript/infra/config/environments/mainnet3/ism/verification.json @@ -2244,5 +2244,67 @@ "constructorArguments": "", "isProxy": true } + ], + "inevm": [ + { + "name": "MerkleRootMultisigIsmFactory", + "address": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMerkleRootMultisigIsm", + "address": "0x4725F7b8037513915aAf6D6CBDE2920E28540dDc", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "MessageIdMultisigIsmFactory", + "address": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticMessageIdMultisigIsm", + "address": "0xAF03386044373E2fe26C5b1dCedF5a7e854a7a3F", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "AggregationIsmFactory", + "address": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationIsm", + "address": "0x882CD0C5D50b6dD74b36Da4BDb059507fddEDdf2", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "AggregationHookFactory", + "address": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "StaticAggregationHook", + "address": "0x19930232E9aFC4f4F09d09fe2375680fAc2100D0", + "constructorArguments": "", + "isProxy": true + }, + { + "name": "RoutingIsmFactory", + "address": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "constructorArguments": "", + "isProxy": false + }, + { + "name": "DomaingRoutingIsm", + "address": "0x12Ed1BbA182CbC63692F813651BD493B7445C874", + "constructorArguments": "", + "isProxy": true + } ] } diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index b01cb0ed7e..9794189456 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -1,5 +1,7 @@ import { ChainMap, OwnableConfig } from '@hyperlane-xyz/sdk'; -import { Address, objMap } from '@hyperlane-xyz/utils'; +import { Address } from '@hyperlane-xyz/utils'; + +import { ethereumChainNames } from './chains'; export const timelocks: ChainMap
= { arbitrum: '0xAC98b0cD1B64EA4fe133C6D2EDaf842cE5cF4b01', @@ -16,19 +18,18 @@ export const safes: ChainMap
= { moonbeam: '0xF0cb1f968Df01fc789762fddBfA704AE0F952197', gnosis: '0x36b0AA0e7d04e7b825D7E409FEa3c9A3d57E4C22', // solana: 'EzppBFV2taxWw8kEjxNYvby6q7W1biJEqwP3iC7YgRe3', - // TODO: create gnosis safes here - base: undefined, - scroll: undefined, - polygonzkevm: undefined, - mantapacific: undefined, - viction: undefined, }; const deployer = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; -export const owners: ChainMap = objMap(safes, (local, __) => ({ - owner: deployer, // TODO: change this to the safe - ownerOverrides: { - proxyAdmin: timelocks[local] ?? safes[local] ?? deployer, - }, -})); +export const owners: ChainMap = Object.fromEntries( + ethereumChainNames.map((local) => [ + local, + { + owner: deployer, // TODO: change this to the safe + ownerOverrides: { + proxyAdmin: timelocks[local] ?? safes[local] ?? deployer, + }, + }, + ]), +); diff --git a/typescript/infra/config/environments/mainnet3/validators.ts b/typescript/infra/config/environments/mainnet3/validators.ts index e7fe44e984..273e3ae412 100644 --- a/typescript/infra/config/environments/mainnet3/validators.ts +++ b/typescript/infra/config/environments/mainnet3/validators.ts @@ -175,6 +175,38 @@ export const validatorChainConfig = ( 'base', ), }, + injective: { + interval: 5, + reorgPeriod: getReorgPeriod(chainMetadata.injective), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: [ + '0xbfb8911b72cfb138c7ce517c57d9c691535dc517', + '0x6faa139c33a7e6f53cb101f6b2ae392298283ed2', + '0x0115e3a66820fb99da30d30e2ce52a453ba99d92', + ], + [Contexts.ReleaseCandidate]: [], + [Contexts.Neutron]: [], + }, + 'injective', + ), + }, + inevm: { + interval: 5, + reorgPeriod: getReorgPeriod(chainMetadata.inevm), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: [ + '0xf9e35ee88e4448a3673b4676a4e153e3584a08eb', + '0xae3e6bb6b3ece1c425aa6f47adc8cb0453c1f9a2', + '0xd98c9522cd9d3e3e00bee05ff76c34b91b266ec3', + ], + [Contexts.ReleaseCandidate]: [], + [Contexts.Neutron]: [], + }, + 'inevm', + ), + }, scroll: { interval: 5, reorgPeriod: getReorgPeriod(chainMetadata.scroll), diff --git a/typescript/infra/config/environments/mainnet3/warp/addresses.json b/typescript/infra/config/environments/mainnet3/warp/addresses.json index 79b01185bb..a84f3a7094 100644 --- a/typescript/infra/config/environments/mainnet3/warp/addresses.json +++ b/typescript/infra/config/environments/mainnet3/warp/addresses.json @@ -1,5 +1,5 @@ { - "arbitrum": { - "router": "0x93ca0d85837FF83158Cd14D65B169CdB223b1921" + "inevm": { + "router": "0x26f32245fCF5Ad53159E875d5Cae62aEcf19c2d4" } } diff --git a/typescript/infra/config/environments/testnet4/validators.ts b/typescript/infra/config/environments/testnet4/validators.ts index 90677f9aa2..3617f04796 100644 --- a/typescript/infra/config/environments/testnet4/validators.ts +++ b/typescript/infra/config/environments/testnet4/validators.ts @@ -289,6 +289,18 @@ export const validatorChainConfig = ( 'polygonzkevmtestnet', ), }, + injective: { + interval: 5, + reorgPeriod: getReorgPeriod(chainMetadata.injective), + validators: validatorsConfig( + { + [Contexts.Hyperlane]: ['0x10686BEe585491A0DA5bfCd5ABfbB95Ab4d6c86d'], + [Contexts.ReleaseCandidate]: [], + [Contexts.Neutron]: [], + }, + 'injective', + ), + }, // proteustestnet: { // interval: 5, // reorgPeriod: getReorgPeriod(chainMetadata.proteustestnet), diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index 9447f4d24a..f8a36a58df 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -17,6 +17,7 @@ import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { Contexts } from '../config/contexts'; import { environments } from '../config/environments'; +import { ethereumChainNames } from '../config/environments/mainnet3/chains'; import { getCurrentKubernetesContext } from '../src/agents'; import { getCloudAgentKey } from '../src/agents/key-utils'; import { CloudAgentKey } from '../src/agents/keys'; @@ -203,11 +204,17 @@ export async function getMultiProviderForRole( const multiProvider = new MultiProvider(txConfigs); await promiseObjAll( objMap(txConfigs, async (chain, _) => { - const provider = await fetchProvider(environment, chain, connectionType); - const key = getKeyForRole(environment, context, chain, role, index); - const signer = await key.getSigner(provider); - multiProvider.setProvider(chain, provider); - multiProvider.setSigner(chain, signer); + if (ethereumChainNames.includes(chain)) { + const provider = await fetchProvider( + environment, + chain, + connectionType, + ); + const key = getKeyForRole(environment, context, chain, role, index); + const signer = await key.getSigner(provider); + multiProvider.setProvider(chain, provider); + multiProvider.setSigner(chain, signer); + } }), ); diff --git a/typescript/sdk/src/consts/chainMetadata.ts b/typescript/sdk/src/consts/chainMetadata.ts index 7e9b91b76c..458c2e6e48 100644 --- a/typescript/sdk/src/consts/chainMetadata.ts +++ b/typescript/sdk/src/consts/chainMetadata.ts @@ -419,6 +419,58 @@ export const gnosis: ChainMetadata = { ], }; +export const inevm: ChainMetadata = { + blockExplorers: [ + { + apiUrl: 'https://inevm.calderaexplorer.xyz/api', + family: ExplorerFamily.Blockscout, + name: 'Caldera inEVM Explorer', + url: 'https://inevm.calderaexplorer.xyz', + }, + ], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 0, + }, + chainId: 2525, + displayName: 'Injective EVM', + displayNameShort: 'inEVM', + domainId: 2525, + name: Chains.inevm, + nativeToken: { + decimals: 18, + name: 'Injective', + symbol: 'INJ', + }, + protocol: ProtocolType.Ethereum, + rpcUrls: [{ http: 'https://inevm.calderachain.xyz/http' }], +}; + +export const injective: ChainMetadata = { + bech32Prefix: 'inj', + blockExplorers: [], + blocks: { + confirmations: 1, + estimateBlockTime: 3, + reorgPeriod: 1, + }, + chainId: 'injective-1', + displayName: 'Injective', + domainId: 6909546, + grpcUrls: [{ http: 'sentry.chain.grpc.injective.network:443' }], + name: Chains.injective, + nativeToken: { + decimals: 18, + name: 'Injective', + symbol: 'INJ', + }, + protocol: ProtocolType.Cosmos, + restUrls: [{ http: 'https://sentry.lcd.injective.network:443' }], + rpcUrls: [{ http: 'https://sentry.tm.injective.network:443' }], + slip44: 118, +}; + export const lineagoerli: ChainMetadata = { blockExplorers: [ { @@ -1068,6 +1120,8 @@ export const chainMetadata: ChainMap = { fuji, gnosis, goerli, + inevm, + injective, lineagoerli, mantapacific, moonbasealpha, diff --git a/typescript/sdk/src/consts/chains.ts b/typescript/sdk/src/consts/chains.ts index 1711c1d107..09b3749811 100644 --- a/typescript/sdk/src/consts/chains.ts +++ b/typescript/sdk/src/consts/chains.ts @@ -17,6 +17,8 @@ export enum Chains { fuji = 'fuji', gnosis = 'gnosis', goerli = 'goerli', + inevm = 'inevm', + injective = 'injective', lineagoerli = 'lineagoerli', mantapacific = 'mantapacific', moonbasealpha = 'moonbasealpha', @@ -71,6 +73,8 @@ export const Mainnets: Array = [ Chains.base, Chains.scroll, Chains.polygonzkevm, + Chains.injective, + Chains.inevm, Chains.viction, // Chains.solana, ]; diff --git a/typescript/sdk/src/consts/environments/mainnet.json b/typescript/sdk/src/consts/environments/mainnet.json index 041af75f41..82f22b523a 100644 --- a/typescript/sdk/src/consts/environments/mainnet.json +++ b/typescript/sdk/src/consts/environments/mainnet.json @@ -257,6 +257,26 @@ "fallbackRoutingHook": "0xD1E267d2d7876e97E217BfE61c34AB50FEF52807", "interchainSecurityModule": "0xDEed16fe4b1c9b2a93483EDFf34C77A9b57D31Ff" }, + "inevm": { + "merkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", + "messageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", + "aggregationIsmFactory": "0x8F7454AC98228f3504Bb91eA3D8Adafe6406110A", + "aggregationHookFactory": "0xEb9FcFDC9EfDC17c1EC5E1dc085B98485da213D6", + "routingIsmFactory": "0x1052eF3419f26Bec74Ed7CEf4a4FA6812Bc09908", + "domainRoutingIsm": "0xBD70Ea9D599a0FC8158B026797177773C3445730", + "proxyAdmin": "0x0761b0827849abbf7b0cC09CE14e1C93D87f5004", + "storageGasOracle": "0x6119E37Bd66406A1Db74920aC79C15fB8411Ba76", + "interchainGasPaymaster": "0x19dc38aeae620380430C200a6E990D5Af5480117", + "merkleTreeHook": "0x0972954923a1e2b2aAb04Fa0c4a0797e5989Cd65", + "aggregationHook": "0xe0dDb5dE7D52918237cC1Ae131F29dcAbcb0F62B", + "protocolFee": "0x0D63128D887159d63De29497dfa45AFc7C699AE4", + "mailbox": "0x2f2aFaE1139Ce54feFC03593FeE8AB2aDF4a85A7", + "validatorAnnounce": "0x15ab173bDB6832f9b64276bA128659b0eD77730B", + "interchainSecurityModule": "0x3052aD50De54aAAc5D364d80bBE681d29e924597", + "pausableIsm": "0x6Fae4D9935E2fcb11fC79a64e917fb2BF14DaFaa", + "staticAggregationIsm": "0x3052aD50De54aAAc5D364d80bBE681d29e924597", + "pausableHook": "0xBDa330Ea8F3005C421C8088e638fBB64fA71b9e0" + }, "viction": { "merkleRootMultisigIsmFactory": "0x2C1FAbEcd7bFBdEBF27CcdB67baADB38b6Df90fC", "messageIdMultisigIsmFactory": "0x8b83fefd896fAa52057798f6426E9f0B080FCCcE", diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index fb507c693e..2d2bde114f 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -143,6 +143,24 @@ export const defaultMultisigConfigs: ChainMap = { ], }, + inevm: { + threshold: 2, + validators: [ + '0xf9e35ee88e4448a3673b4676a4e153e3584a08eb', + '0xae3e6bb6b3ece1c425aa6f47adc8cb0453c1f9a2', + '0xd98c9522cd9d3e3e00bee05ff76c34b91b266ec3', + ], + }, + + injective: { + threshold: 2, + validators: [ + '0xbfb8911b72cfb138c7ce517c57d9c691535dc517', + '0x6faa139c33a7e6f53cb101f6b2ae392298283ed2', + '0x0115e3a66820fb99da30d30e2ce52a453ba99d92', + ], + }, + lineagoerli: { threshold: 2, validators: [