From 8687e56c8083d591bbd6d454265f0cec095f7671 Mon Sep 17 00:00:00 2001 From: J M Rossy Date: Wed, 4 Oct 2023 13:07:44 -0400 Subject: [PATCH] Fixes for multi-protocol Kathy (#2738) ### Description - Adapter fixes from further testing of multi-protocol Kathy - Add router address for hyperlane context helloworld - Improve stat collection to handle cross-protocol msgs - Re-enable actual env config for Kathy ### Drive-by changes - Fix for transaction hash checking in utils ### Related issues https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2503 ### Backward compatibility Yes ### Testing Ran Kathy manually using `getCoreConfigStub` and forced stat output Tested live in cloud with help from @tkporter --------- Co-authored-by: Trevor Porter --- README.md | 2 +- typescript/helloworld/src/app/types.ts | 4 +- .../src/multiProtocolApp/evmAdapter.ts | 16 +----- .../src/multiProtocolApp/multiProtocolApp.ts | 14 ++++-- .../src/multiProtocolApp/sealevelAdapter.ts | 15 +++--- .../helloworld/src/multiProtocolApp/types.ts | 9 +--- .../environments/mainnet2/helloworld.ts | 6 +-- .../environments/testnet3/helloworld.ts | 6 +-- .../templates/external-secret.yaml | 16 +++--- typescript/infra/scripts/check-deploy.ts | 2 +- typescript/infra/scripts/helloworld/deploy.ts | 2 +- typescript/infra/scripts/helloworld/kathy.ts | 18 ++++--- typescript/infra/scripts/helloworld/utils.ts | 47 +++++++++++++++--- typescript/infra/scripts/utils.ts | 39 ++------------- typescript/infra/src/agents/key-utils.ts | 49 ++++++++++++++++--- typescript/infra/src/config/environment.ts | 2 +- .../infra/src/config/helloworld/config.ts | 27 ++++++++++ .../{helloworld.ts => helloworld/types.ts} | 28 +---------- typescript/infra/src/config/index.ts | 2 +- typescript/infra/src/helloworld/kathy.ts | 12 ++++- .../src/core/adapters/SealevelCoreAdapter.ts | 23 ++++++--- typescript/utils/src/addresses.ts | 16 +++--- 22 files changed, 202 insertions(+), 153 deletions(-) create mode 100644 typescript/infra/src/config/helloworld/config.ts rename typescript/infra/src/config/{helloworld.ts => helloworld/types.ts} (57%) diff --git a/README.md b/README.md index 68830a86db..ad2058a9fe 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Hyperlane is an interchain messaging protocol that allows applications to commun Developers can use Hyperlane to share state between blockchains, allowing them to build interchain applications that live natively across multiple chains. -To read more about interchain applications, how the protocol works, and how to integrate with Hyperlane, please see the [documentation](https://docs.hyperlane.xyz/). +To read more about interchain applications, how the protocol works, and how to integrate with Hyperlane, please see the [documentation](https://docs.hyperlane.xyz). ## Working on Hyperlane diff --git a/typescript/helloworld/src/app/types.ts b/typescript/helloworld/src/app/types.ts index 52483d4cd7..8b9800adac 100644 --- a/typescript/helloworld/src/app/types.ts +++ b/typescript/helloworld/src/app/types.ts @@ -1,4 +1,4 @@ export type StatCounts = { - sent: number | bigint; - received: number | bigint; + sent: number; + received: number; }; diff --git a/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts b/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts index 43a7dc6fa2..d13667672c 100644 --- a/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts +++ b/typescript/helloworld/src/multiProtocolApp/evmAdapter.ts @@ -7,7 +7,6 @@ import { } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; -import { StatCounts } from '../app/types'; import { HelloWorld, HelloWorld__factory } from '../types'; import { IHelloWorldAdapter } from './types'; @@ -55,22 +54,11 @@ export class EvmHelloWorldAdapter return { transaction: tx, type: ProviderType.EthersV5 }; } - async channelStats( - destination: ChainName, - destinationMailbox: Address, - ): Promise { - const originDomain = this.multiProvider.getDomainId(this.chainName); + async sentStat(destination: ChainName): Promise { const destinationDomain = this.multiProvider.getDomainId(destination); const originContract = this.getConnectedContract(); const sent = await originContract.sentTo(destinationDomain); - const destinationProvider = - this.multiProvider.getEthersV5Provider(destination); - const destinationContract = HelloWorld__factory.connect( - destinationMailbox, - destinationProvider, - ); - const received = await destinationContract.sentTo(originDomain); - return { sent: sent.toNumber(), received: received.toNumber() }; + return sent.toNumber(); } override getConnectedContract(): HelloWorld { diff --git a/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts b/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts index f969acbcd2..39a0964e71 100644 --- a/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts +++ b/typescript/helloworld/src/multiProtocolApp/multiProtocolApp.ts @@ -38,11 +38,15 @@ export class HelloMultiProtocolApp extends MultiProtocolRouterApp< ); } - channelStats(origin: ChainName, destination: ChainName): Promise { - return this.adapter(origin).channelStats( - destination, - this.addresses[destination].mailbox, - ); + async channelStats( + origin: ChainName, + destination: ChainName, + ): Promise { + const [sent, received] = await Promise.all([ + this.adapter(origin).sentStat(destination), + this.adapter(destination).sentStat(origin), + ]); + return { sent, received }; } async stats(): Promise>> { diff --git a/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts b/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts index 22bea23e59..c3b3fcb2fa 100644 --- a/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts +++ b/typescript/helloworld/src/multiProtocolApp/sealevelAdapter.ts @@ -24,8 +24,6 @@ import { } from '@hyperlane-xyz/sdk'; import { Address, Domain } from '@hyperlane-xyz/utils'; -import { StatCounts } from '../app/types'; - import { IHelloWorldAdapter } from './types'; export class SealevelHelloWorldAdapter @@ -164,21 +162,20 @@ export class SealevelHelloWorldAdapter ); } - async channelStats(_destination: ChainName): Promise { + async sentStat(destination: ChainName): Promise { + const destinationDomain = this.multiProvider.getDomainId(destination); const data = await this.getAccountInfo(); - return { sent: data.sent, received: data.received }; + return Number(data.sent_to.get(destinationDomain) || 0); } async getAccountInfo(): Promise { const address = this.addresses.router; const connection = this.getProvider(); - const msgRecipientPda = this.deriveMessageRecipientPda(address); - const accountInfo = await connection.getAccountInfo(msgRecipientPda); + const pda = this.deriveProgramStoragePDA(address); + const accountInfo = await connection.getAccountInfo(pda); if (!accountInfo) - throw new Error( - `No account info found for ${msgRecipientPda.toBase58()}}`, - ); + throw new Error(`No account info found for ${pda.toBase58()}}`); const accountData = deserializeUnchecked( HelloWorldDataSchema, diff --git a/typescript/helloworld/src/multiProtocolApp/types.ts b/typescript/helloworld/src/multiProtocolApp/types.ts index 2f9eb5e5c3..06d7757689 100644 --- a/typescript/helloworld/src/multiProtocolApp/types.ts +++ b/typescript/helloworld/src/multiProtocolApp/types.ts @@ -5,8 +5,6 @@ import { } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; -import { StatCounts } from '../app/types'; - export interface IHelloWorldAdapter extends IRouterAdapter { populateSendHelloTx: ( destination: ChainName, @@ -15,10 +13,5 @@ export interface IHelloWorldAdapter extends IRouterAdapter { sender: Address, ) => Promise; - // TODO break apart into separate origin + destination methods to - // handle case where origin/dest protocols differ - channelStats: ( - destination: ChainName, - destinationMailbox: Address, - ) => Promise; + sentStat: (destination: ChainName) => Promise; } diff --git a/typescript/infra/config/environments/mainnet2/helloworld.ts b/typescript/infra/config/environments/mainnet2/helloworld.ts index dbc6cee4aa..b1aebaa592 100644 --- a/typescript/infra/config/environments/mainnet2/helloworld.ts +++ b/typescript/infra/config/environments/mainnet2/helloworld.ts @@ -1,7 +1,7 @@ import { AgentConnectionType } from '@hyperlane-xyz/sdk'; import { HelloWorldConfig } from '../../../src/config'; -import { HelloWorldKathyRunMode } from '../../../src/config/helloworld'; +import { HelloWorldKathyRunMode } from '../../../src/config/helloworld/types'; import { Contexts } from '../../contexts'; import { environment } from './chains'; @@ -13,7 +13,7 @@ export const hyperlane: HelloWorldConfig = { kathy: { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '4c598b9-20230503-205323', + tag: '25da8e0-20231004-135818', }, chainsToSkip: [], runEnv: environment, @@ -34,7 +34,7 @@ export const releaseCandidate: HelloWorldConfig = { kathy: { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '25f19b7-20230319-124624', + tag: '25da8e0-20231004-135818', }, chainsToSkip: [], runEnv: environment, diff --git a/typescript/infra/config/environments/testnet3/helloworld.ts b/typescript/infra/config/environments/testnet3/helloworld.ts index f6e50e030c..b5b20c6e7e 100644 --- a/typescript/infra/config/environments/testnet3/helloworld.ts +++ b/typescript/infra/config/environments/testnet3/helloworld.ts @@ -1,7 +1,7 @@ import { AgentConnectionType } from '@hyperlane-xyz/sdk'; import { HelloWorldConfig } from '../../../src/config'; -import { HelloWorldKathyRunMode } from '../../../src/config/helloworld'; +import { HelloWorldKathyRunMode } from '../../../src/config/helloworld/types'; import { Contexts } from '../../contexts'; import { environment } from './chains'; @@ -13,7 +13,7 @@ export const hyperlaneHelloworld: HelloWorldConfig = { kathy: { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '4c598b9-20230503-205323', + tag: '25da8e0-20231004-135818', }, chainsToSkip: [], runEnv: environment, @@ -33,7 +33,7 @@ export const releaseCandidateHelloworld: HelloWorldConfig = { kathy: { docker: { repo: 'gcr.io/abacus-labs-dev/hyperlane-monorepo', - tag: '25f19b7-20230319-124624', + tag: '25da8e0-20231004-135818', }, chainsToSkip: [], runEnv: environment, diff --git a/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml b/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml index 28af4157ec..dff0d47979 100644 --- a/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml +++ b/typescript/infra/helm/helloworld-kathy/templates/external-secret.yaml @@ -22,12 +22,16 @@ spec: data: GCP_SECRET_OVERRIDES_ENABLED: "true" GCP_SECRET_OVERRIDE_HYPERLANE_{{ .Values.hyperlane.runEnv | upper }}_KEY_DEPLOYER: {{ print "'{{ .deployer_key | toString }}'" }} -{{/* + {{/* + * Always get the GCP-based key, which is used for non-EVM chains. + */}} + GCP_SECRET_OVERRIDE_{{ .Values.hyperlane.context | upper }}_{{ .Values.hyperlane.runEnv | upper }}_KEY_KATHY: {{ print "'{{ .gcp_private_key | toString }}'" }} + {{/* * For each network, create an environment variable with the RPC endpoint. * The templating of external-secrets will use the data section below to know how * to replace the correct value in the created secret. */}} - {{- range .Values.hyperlane.chains }} + {{- range .Values.hyperlane.chains.relayer }} {{- if or (eq $.Values.hyperlane.connectionType "httpQuorum") (eq $.Values.hyperlane.connectionType "httpFallback") }} GCP_SECRET_OVERRIDE_{{ $.Values.hyperlane.runEnv | upper }}_RPC_ENDPOINTS_{{ . | upper }}: {{ printf "'{{ .%s_rpcs | toString }}'" . }} {{- else }} @@ -37,8 +41,6 @@ spec: {{- if .Values.hyperlane.aws }} AWS_ACCESS_KEY_ID: {{ print "'{{ .aws_access_key_id | toString }}'" }} AWS_SECRET_ACCESS_KEY: {{ print "'{{ .aws_secret_access_key | toString }}'" }} - {{- else }} - GCP_SECRET_OVERRIDE_{{ .Values.hyperlane.context | upper }}_{{ .Values.hyperlane.runEnv | upper }}_KEY_KATHY: {{ print "'{{ .gcp_private_key | toString }}'" }} {{- end }} data: - secretKey: deployer_key @@ -48,7 +50,7 @@ spec: * For each network, load the secret in GCP secret manager with the form: environment-rpc-endpoint-network, * and associate it with the secret key networkname_rpc. */}} - {{- range .Values.hyperlane.chains }} + {{- range .Values.hyperlane.chains.relayer }} {{- if or (eq $.Values.hyperlane.connectionType "httpQuorum") (eq $.Values.hyperlane.connectionType "httpFallback") }} - secretKey: {{ printf "%s_rpcs" . }} remoteRef: @@ -66,9 +68,7 @@ spec: - secretKey: aws_secret_access_key remoteRef: key: {{ printf "%s-%s-kathy-aws-secret-access-key" .Values.hyperlane.context .Values.hyperlane.runEnv }} - {{- else }} + {{- end }} - secretKey: gcp_private_key remoteRef: key: {{ printf "%s-%s-key-kathy" $.Values.hyperlane.context $.Values.hyperlane.runEnv }} - property: privateKey - {{- end }} diff --git a/typescript/infra/scripts/check-deploy.ts b/typescript/infra/scripts/check-deploy.ts index c9a0e3ed18..2b570c4779 100644 --- a/typescript/infra/scripts/check-deploy.ts +++ b/typescript/infra/scripts/check-deploy.ts @@ -15,7 +15,7 @@ import { ProtocolType } from '@hyperlane-xyz/utils'; import { Contexts } from '../config/contexts'; import { deployEnvToSdkEnv } from '../src/config/environment'; -import { helloWorldRouterConfig } from '../src/config/helloworld'; +import { helloWorldRouterConfig } from '../src/config/helloworld/config'; import { HyperlaneAppGovernor } from '../src/govern/HyperlaneAppGovernor'; import { HyperlaneCoreGovernor } from '../src/govern/HyperlaneCoreGovernor'; import { HyperlaneIgpGovernor } from '../src/govern/HyperlaneIgpGovernor'; diff --git a/typescript/infra/scripts/helloworld/deploy.ts b/typescript/infra/scripts/helloworld/deploy.ts index e88258971e..9d4fc317e0 100644 --- a/typescript/infra/scripts/helloworld/deploy.ts +++ b/typescript/infra/scripts/helloworld/deploy.ts @@ -15,7 +15,7 @@ import { import { Contexts } from '../../config/contexts'; import { deployEnvToSdkEnv } from '../../src/config/environment'; -import { helloWorldRouterConfig } from '../../src/config/helloworld'; +import { helloWorldRouterConfig } from '../../src/config/helloworld/config'; import { Role } from '../../src/roles'; import { readJSON, writeJSON } from '../../src/utils/utils'; import { diff --git a/typescript/infra/scripts/helloworld/kathy.ts b/typescript/infra/scripts/helloworld/kathy.ts index cdd952f111..e1db788073 100644 --- a/typescript/infra/scripts/helloworld/kathy.ts +++ b/typescript/infra/scripts/helloworld/kathy.ts @@ -23,6 +23,7 @@ import { error, log, retryAsync, + strip0x, timeout, warn, } from '@hyperlane-xyz/utils'; @@ -37,7 +38,7 @@ import { DeployEnvironment } from '../../src/config/environment'; import { Role } from '../../src/roles'; import { startMetricsServer } from '../../src/utils/metrics'; import { assertChain, diagonalize, sleep } from '../../src/utils/utils'; -import { getAddressesForKey, getArgs, withContext } from '../utils'; +import { getArgs, getEnvironmentConfig, withContext } from '../utils'; import { getHelloWorldMultiProtocolApp } from './utils'; @@ -160,9 +161,8 @@ async function main(): Promise { startMetricsServer(metricsRegister); debug('Starting up', { environment }); - // TODO (Rossy) remove getCoreConfigStub and re-enable getEnvironmentConfig - // const coreConfig = getEnvironmentConfig(environment); - const coreConfig = getCoreConfigStub(environment); + const coreConfig = getEnvironmentConfig(environment); + // const coreConfig = getCoreConfigStub(environment); const { app, core, igp, multiProvider, keys } = await getHelloWorldMultiProtocolApp( @@ -421,7 +421,13 @@ async function sendMessage( }); const sendAndConfirmMsg = async () => { - const sender = getAddressesForKey(keys, origin, multiProvider); + const originProtocol = app.metadata(origin).protocol; + const sender = keys[origin].addressForProtocol(originProtocol); + if (!sender) { + throw new Error( + `No sender address found for chain ${origin} and protocol ${originProtocol}`, + ); + } const tx = await app.populateHelloWorldTx( origin, destination, @@ -447,7 +453,7 @@ async function sendMessage( // that have not yet been ported over const connection = app.multiProvider.getSolanaWeb3Provider(origin); const payer = Keypair.fromSeed( - Buffer.from(keys[origin].privateKey, 'hex'), + Buffer.from(strip0x(keys[origin].privateKey), 'hex'), ); tx.transaction.partialSign(payer); // Note, tx signature essentially tx means hash on sealevel diff --git a/typescript/infra/scripts/helloworld/utils.ts b/typescript/infra/scripts/helloworld/utils.ts index 58e6325a55..dd1319ca68 100644 --- a/typescript/infra/scripts/helloworld/utils.ts +++ b/typescript/infra/scripts/helloworld/utils.ts @@ -17,13 +17,14 @@ import { hyperlaneEnvironments, igpFactories, } from '@hyperlane-xyz/sdk'; -import { ProtocolType, objMerge } from '@hyperlane-xyz/utils'; +import { ProtocolType, objMap } from '@hyperlane-xyz/utils'; import { Contexts } from '../../config/contexts'; import { EnvironmentConfig } from '../../src/config'; import { deployEnvToSdkEnv } from '../../src/config/environment'; -import { HelloWorldConfig } from '../../src/config/helloworld'; +import { HelloWorldConfig } from '../../src/config/helloworld/types'; import { Role } from '../../src/roles'; +import { getKeyForRole } from '../utils'; export async function getHelloWorldApp( coreConfig: EnvironmentConfig, @@ -71,8 +72,13 @@ export async function getHelloWorldMultiProtocolApp( connectionType, ); const sdkEnvName = deployEnvToSdkEnv[coreConfig.environment]; + const envAddresses = hyperlaneEnvironments[sdkEnvName]; const keys = await coreConfig.getKeys(keyContext, keyRole); + // Fetch all the keys, which is required to get the address for + // certain cloud keys + await Promise.all(Object.values(keys).map((key) => key.fetch())); + const helloworldConfig = getHelloWorldConfig(coreConfig, context); const multiProtocolProvider = @@ -80,18 +86,46 @@ export async function getHelloWorldMultiProtocolApp( // Hacking around infra code limitations, we may need to add solana manually // because the it's not in typescript/infra/config/environments/testnet3/chains.ts // Adding it there breaks many things - if (!multiProtocolProvider.getKnownChainNames().includes('solanadevnet')) { + if ( + coreConfig.environment === 'testnet3' && + !multiProtocolProvider.getKnownChainNames().includes('solanadevnet') + ) { + multiProvider.addChain(chainMetadata.solanadevnet); multiProtocolProvider.addChain(chainMetadata.solanadevnet); + keys['solanadevnet'] = getKeyForRole( + coreConfig.environment, + context, + 'solanadevnet', + keyRole, + ); + await keys['solanadevnet'].fetch(); + } else if ( + coreConfig.environment === 'mainnet2' && + !multiProtocolProvider.getKnownChainNames().includes('solana') + ) { + multiProvider.addChain(chainMetadata.solana); + multiProtocolProvider.addChain(chainMetadata.solana); + keys['solana'] = getKeyForRole( + coreConfig.environment, + context, + 'solana', + keyRole, + ); + await keys['solana'].fetch(); } const core = MultiProtocolCore.fromAddressesMap( - hyperlaneEnvironments[sdkEnvName], + envAddresses, multiProtocolProvider, ); - const routersAndMailboxes = objMerge( - core.chainMap, + const routersAndMailboxes = objMap( helloworldConfig.addresses, + (chain, addresses) => ({ + router: addresses.router, + // @ts-ignore allow loosely typed chain name to index env addresses + mailbox: envAddresses[chain].mailbox, + }), ); const app = new HelloMultiProtocolApp( multiProtocolProvider, @@ -101,7 +135,6 @@ export async function getHelloWorldMultiProtocolApp( // TODO we need a MultiProtocolIgp // Using an standard IGP for just evm chains for now // Unfortunately this requires hacking surgically around certain addresses - const envAddresses = hyperlaneEnvironments[sdkEnvName]; const filteredAddresses = filterChainMapToProtocol( envAddresses, ProtocolType.Ethereum, diff --git a/typescript/infra/scripts/utils.ts b/typescript/infra/scripts/utils.ts index 55688eea77..f0e299ff5e 100644 --- a/typescript/infra/scripts/utils.ts +++ b/typescript/infra/scripts/utils.ts @@ -1,5 +1,3 @@ -import { Keypair } from '@solana/web3.js'; -import { Wallet } from 'ethers'; import path from 'path'; import yargs from 'yargs'; @@ -8,7 +6,6 @@ import { AllChains, ChainMap, ChainMetadata, - ChainMetadataManager, ChainName, Chains, CoreConfig, @@ -19,12 +16,7 @@ import { RouterConfig, collectValidators, } from '@hyperlane-xyz/sdk'; -import { - ProtocolType, - objMap, - promiseObjAll, - strip0x, -} from '@hyperlane-xyz/utils'; +import { ProtocolType, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { Contexts } from '../config/contexts'; import { environments } from '../config/environments'; @@ -167,7 +159,7 @@ export function getAgentConfig( return agentConfig; } -function getKeyForRole( +export function getKeyForRole( environment: DeployEnvironment, context: Contexts, chain: ChainName, @@ -218,34 +210,13 @@ export async function getKeysForRole( } const keys = await promiseObjAll( - objMap(txConfigs, async (chain, _) => { - const key = getKeyForRole(environment, context, chain, role, index); - if (!key.privateKey) - throw new Error(`Key for ${chain} does not have private key`); - return key; - }), + objMap(txConfigs, async (chain, _) => + getKeyForRole(environment, context, chain, role, index), + ), ); return keys; } -// Note: this will only work for keystores that allow key's to be extracted. -export function getAddressesForKey( - keys: ChainMap, - chain: ChainName, - manager: ChainMetadataManager, -) { - const protocol = manager.getChainMetadata(chain).protocol; - if (protocol === ProtocolType.Ethereum) { - return new Wallet(keys[chain]).address; - } else if (protocol === ProtocolType.Sealevel) { - return Keypair.fromSeed( - Buffer.from(strip0x(keys[chain].privateKey), 'hex'), - ).publicKey.toBase58(); - } else { - throw Error(`Protocol ${protocol} not supported`); - } -} - export function getContractAddressesSdkFilepath() { return path.join('../sdk/src/consts/environments'); } diff --git a/typescript/infra/src/agents/key-utils.ts b/typescript/infra/src/agents/key-utils.ts index 547b2f0152..e7afc68ba7 100644 --- a/typescript/infra/src/agents/key-utils.ts +++ b/typescript/infra/src/agents/key-utils.ts @@ -31,11 +31,11 @@ export function getRelayerCloudAgentKeys( const keys = []; keys.push(new AgentAwsKey(agentConfig, Role.Relayer)); - const nonEthereumChains = agentConfig.contextChainNames[Role.Relayer].find( - (chainName) => chainMetadata[chainName].protocol !== ProtocolType.Ethereum, - ); + const hasNonEthereumChains = !!agentConfig.contextChainNames[ + Role.Relayer + ].find(isNotEthereumProtocolChain); // If there are any non-ethereum chains, we also want hex keys. - if (nonEthereumChains) { + if (hasNonEthereumChains) { keys.push( new AgentGCPKey(agentConfig.runEnv, agentConfig.context, Role.Relayer), ); @@ -43,6 +43,23 @@ export function getRelayerCloudAgentKeys( return keys; } +export function getKathyCloudAgentKeys( + agentConfig: AgentContextConfig, +): Array { + const gcpKey = new AgentGCPKey( + agentConfig.runEnv, + agentConfig.context, + Role.Kathy, + ); + if (!agentConfig.aws) { + return [gcpKey]; + } + + // Return both GCP and AWS keys for Kathy even if the agentConfig is configured + // to use AWS. Non-Ethereum chains require GCP keys. + return [gcpKey, new AgentAwsKey(agentConfig, Role.Kathy)]; +} + // If getting all keys for relayers or validators, it's recommended to use // `getRelayerCloudAgentKeys` or `getValidatorCloudAgentKeys` instead. export function getCloudAgentKey( @@ -51,10 +68,19 @@ export function getCloudAgentKey( chainName?: ChainName, index?: number, ): CloudAgentKey { - // The deployer is always GCP-based - if (!!agentConfig.aws && role !== Role.Deployer) { + // Non-evm Kathy is always GCP-based but does not index by chain + if ( + role === Role.Kathy && + chainName && + isNotEthereumProtocolChain(chainName) + ) { + return new AgentGCPKey(agentConfig.runEnv, agentConfig.context, role); + } + // Otherwise use an AWS key except for the deployer + else if (!!agentConfig.aws && role !== Role.Deployer) { return new AgentAwsKey(agentConfig, role, chainName, index); } else { + // Fallback to GCP return new AgentGCPKey( agentConfig.runEnv, agentConfig.context, @@ -88,8 +114,12 @@ export function getAllCloudAgentKeys( keys.push(...getRelayerCloudAgentKeys(agentConfig)); if ((agentConfig.rolesWithKeys ?? []).includes(Role.Validator)) keys.push(...getValidatorCloudAgentKeys(agentConfig)); + if ((agentConfig.rolesWithKeys ?? []).includes(Role.Kathy)) + keys.push(...getKathyCloudAgentKeys(agentConfig)); + for (const role of agentConfig.rolesWithKeys) { - if (role == Role.Relayer || role == Role.Validator) continue; + if (role == Role.Relayer || role == Role.Validator || role == Role.Kathy) + continue; keys.push(getCloudAgentKey(agentConfig, role)); } return keys; @@ -203,3 +233,8 @@ function addressesIdentifier( ) { return `${context}-${environment}-key-addresses`; } + +function isNotEthereumProtocolChain(chainName: ChainName) { + if (!chainMetadata[chainName]) throw new Error(`Unknown chain ${chainName}`); + return chainMetadata[chainName].protocol !== ProtocolType.Ethereum; +} diff --git a/typescript/infra/src/config/environment.ts b/typescript/infra/src/config/environment.ts index e7e2f900f6..bdaf9d1da9 100644 --- a/typescript/infra/src/config/environment.ts +++ b/typescript/infra/src/config/environment.ts @@ -20,7 +20,7 @@ import { Role } from '../roles'; import { RootAgentConfig } from './agent'; import { KeyFunderConfig } from './funding'; import { AllStorageGasOracleConfigs } from './gas-oracle'; -import { HelloWorldConfig } from './helloworld'; +import { HelloWorldConfig } from './helloworld/types'; import { InfrastructureConfig } from './infrastructure'; import { LiquidityLayerRelayerConfig } from './middleware'; diff --git a/typescript/infra/src/config/helloworld/config.ts b/typescript/infra/src/config/helloworld/config.ts new file mode 100644 index 0000000000..ea1ca71286 --- /dev/null +++ b/typescript/infra/src/config/helloworld/config.ts @@ -0,0 +1,27 @@ +import { ChainMap } from '@hyperlane-xyz/sdk'; +import { MultiProvider, RouterConfig } from '@hyperlane-xyz/sdk'; +import { objMap } from '@hyperlane-xyz/utils'; + +import { Contexts } from '../../../config/contexts'; +import { + mainnetHyperlaneDefaultIsmCache, + routingIsm, +} from '../../../config/routingIsm'; +import { getRouterConfig } from '../../../scripts/utils'; +import { DeployEnvironment } from '../environment'; + +export async function helloWorldRouterConfig( + environment: DeployEnvironment, + context: Contexts, + multiProvider: MultiProvider, +): Promise> { + const routerConfig = await getRouterConfig(environment, multiProvider, true); + return objMap(routerConfig, (chain, config) => ({ + ...config, + interchainSecurityModule: + context === Contexts.Hyperlane + ? // TODO move back to `undefined` after these are verified and made the default ISMs + mainnetHyperlaneDefaultIsmCache[chain] + : routingIsm(environment, chain, context), + })); +} diff --git a/typescript/infra/src/config/helloworld.ts b/typescript/infra/src/config/helloworld/types.ts similarity index 57% rename from typescript/infra/src/config/helloworld.ts rename to typescript/infra/src/config/helloworld/types.ts index d004ef7d00..90a922c9e4 100644 --- a/typescript/infra/src/config/helloworld.ts +++ b/typescript/infra/src/config/helloworld/types.ts @@ -1,16 +1,6 @@ import { AgentConnectionType, ChainMap, ChainName } from '@hyperlane-xyz/sdk'; -import { MultiProvider, RouterConfig } from '@hyperlane-xyz/sdk'; -import { objMap } from '@hyperlane-xyz/utils'; -import { Contexts } from '../../config/contexts'; -import { - mainnetHyperlaneDefaultIsmCache, - routingIsm, -} from '../../config/routingIsm'; -import { getRouterConfig } from '../../scripts/utils'; - -import { DockerConfig } from './agent'; -import { DeployEnvironment } from './environment'; +import { DockerConfig } from '../agent'; export enum HelloWorldKathyRunMode { // Sends messages between all pairwise chains @@ -48,19 +38,3 @@ export interface HelloWorldConfig { addresses: ChainMap<{ router: string }>; kathy: HelloWorldKathyConfig; } - -export async function helloWorldRouterConfig( - environment: DeployEnvironment, - context: Contexts, - multiProvider: MultiProvider, -): Promise> { - const routerConfig = await getRouterConfig(environment, multiProvider, true); - return objMap(routerConfig, (chain, config) => ({ - ...config, - interchainSecurityModule: - context === Contexts.Hyperlane - ? // TODO move back to `undefined` after these are verified and made the default ISMs - mainnetHyperlaneDefaultIsmCache[chain] - : routingIsm(environment, chain, context), - })); -} diff --git a/typescript/infra/src/config/index.ts b/typescript/infra/src/config/index.ts index 262d276de2..f98fa4b3be 100644 --- a/typescript/infra/src/config/index.ts +++ b/typescript/infra/src/config/index.ts @@ -5,6 +5,6 @@ export { StorageGasOracleConfig, getAllStorageGasOracleConfigs, } from './gas-oracle'; -export { HelloWorldConfig } from './helloworld'; +export { HelloWorldConfig } from './helloworld/types'; export { InfrastructureConfig } from './infrastructure'; export * from './agent'; diff --git a/typescript/infra/src/helloworld/kathy.ts b/typescript/infra/src/helloworld/kathy.ts index 50610aef49..1ce858b766 100644 --- a/typescript/infra/src/helloworld/kathy.ts +++ b/typescript/infra/src/helloworld/kathy.ts @@ -1,10 +1,11 @@ import { Contexts } from '../../config/contexts'; import { AgentAwsUser } from '../agents/aws'; +import { AgentGCPKey } from '../agents/gcp'; import { AgentContextConfig } from '../config'; import { HelloWorldKathyConfig, HelloWorldKathyRunMode, -} from '../config/helloworld'; +} from '../config/helloworld/types'; import { Role } from '../roles'; import { HelmCommand, helmifyValues } from '../utils/helm'; import { execCmd } from '../utils/utils'; @@ -26,6 +27,15 @@ export async function runHelloworldKathyHelmCommand( await awsUser.createKeyIfNotExists(agentConfig); } + // Also ensure a GCP key exists, which is used for non-EVM chains even if + // the agent config is AWS-based + const kathyKey = new AgentGCPKey( + agentConfig.runEnv, + agentConfig.context, + Role.Kathy, + ); + await kathyKey.createIfNotExists(); + const values = getHelloworldKathyHelmValues(agentConfig, kathyConfig); return execCmd( diff --git a/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts b/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts index 027774d901..1709d9f0f6 100644 --- a/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts +++ b/typescript/sdk/src/core/adapters/SealevelCoreAdapter.ts @@ -1,6 +1,12 @@ import { PublicKey } from '@solana/web3.js'; -import { Address, HexString, pollAsync } from '@hyperlane-xyz/utils'; +import { + Address, + HexString, + ensure0x, + pollAsync, + strip0x, +} from '@hyperlane-xyz/utils'; import { BaseSealevelAdapter } from '../../app/MultiProtocolApp'; import { MultiProtocolProvider } from '../../providers/MultiProtocolProvider'; @@ -35,14 +41,13 @@ export class SealevelCoreAdapter `Unsupported provider type for SealevelCoreAdapter ${sourceTx.type}`, ); } - const receipt = sourceTx.receipt; - const logs = receipt.meta?.logMessages; + const logs = sourceTx.receipt.meta?.logMessages; if (!logs) throw new Error('Transaction logs required to check message delivery'); const parsedLogs = SealevelCoreAdapter.parseMessageDispatchLogs(logs); if (!parsedLogs.length) throw new Error('Message dispatch log not found'); return parsedLogs.map(({ destination, messageId }) => ({ - messageId: Buffer.from(messageId, 'hex').toString('hex'), + messageId: ensure0x(messageId), destination: this.multiProvider.getChainName(destination), })); } @@ -144,10 +149,16 @@ export class SealevelCoreAdapter static deriveMailboxMessageProcessedPda( mailboxProgramId: string | PublicKey, - messageId: string, + messageId: HexString, ): PublicKey { return super.derivePda( - ['hyperlane', '-', 'processed_message', Buffer.from(messageId, 'hex')], + [ + 'hyperlane', + '-', + 'processed_message', + '-', + Buffer.from(strip0x(messageId), 'hex'), + ], mailboxProgramId, ); } diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index f415677275..4ad170803b 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -135,14 +135,14 @@ export function isValidTransactionHashSealevel(input: string) { return SEALEVEL_TX_HASH_REGEX.test(input); } -export function isValidTransactionHash(input: string, protocol?: ProtocolType) { - return routeAddressUtil( - isValidTransactionHashEvm, - isValidTransactionHashSealevel, - false, - input, - protocol, - ); +export function isValidTransactionHash(input: string, protocol: ProtocolType) { + if (protocol === ProtocolType.Ethereum) { + return isValidTransactionHashEvm(input); + } else if (protocol === ProtocolType.Sealevel) { + return isValidTransactionHashSealevel(input); + } else { + return false; + } } export function isZeroishAddress(address: Address) {