diff --git a/rust/chains/hyperlane-cosmos/src/libs/address.rs b/rust/chains/hyperlane-cosmos/src/libs/address.rs index 11e15ff9e3c..226e90eeffd 100644 --- a/rust/chains/hyperlane-cosmos/src/libs/address.rs +++ b/rust/chains/hyperlane-cosmos/src/libs/address.rs @@ -48,9 +48,21 @@ impl CosmosAddress { /// /// - digest: H256 digest (hex representation of address) /// - prefix: Bech32 prefix - pub fn from_h256(digest: H256, prefix: &str) -> ChainResult { + /// - byte_count: Number of bytes to truncate the digest to. Cosmos addresses can sometimes + /// be less than 32 bytes, so this helps to serialize it in bech32 with the appropriate + /// length. + pub fn from_h256(digest: H256, prefix: &str, byte_count: usize) -> ChainResult { // This is the hex-encoded version of the address - let bytes = digest.as_bytes(); + let untruncated_bytes = digest.as_bytes(); + + if byte_count > untruncated_bytes.len() { + return Err(Overflow.into()); + } + + let remainder_bytes_start = untruncated_bytes.len() - byte_count; + // Left-truncate the digest to the desired length + let bytes = &untruncated_bytes[remainder_bytes_start..]; + // Bech32 encode it let account_id = AccountId::new(prefix, bytes).map_err(Into::::into)?; @@ -132,11 +144,13 @@ pub mod test { addr.address(), "neutron1kknekjxg0ear00dky5ykzs8wwp2gz62z9s6aaj" ); - // TODO: watch out for this edge case. This check will fail unless - // the first 12 bytes are removed from the digest. - // let digest = addr.digest(); - // let addr2 = CosmosAddress::from_h256(digest, prefix).expect("Cosmos address creation failed"); - // assert_eq!(addr.address(), addr2.address()); + + // Create an address with the same digest & explicitly set the byte count to 20, + // which should have the same result as the above. + let digest = addr.digest(); + let addr2 = + CosmosAddress::from_h256(digest, prefix, 20).expect("Cosmos address creation failed"); + assert_eq!(addr.address(), addr2.address()); } #[test] @@ -144,10 +158,19 @@ pub mod test { let hex_key = "0x1b16866227825a5166eb44031cdcf6568b3e80b52f2806e01b89a34dc90ae616"; let key = hex_or_base58_to_h256(hex_key).unwrap(); let prefix = "dual"; - let addr = CosmosAddress::from_h256(key, prefix).expect("Cosmos address creation failed"); + let addr = + CosmosAddress::from_h256(key, prefix, 32).expect("Cosmos address creation failed"); assert_eq!( addr.address(), "dual1rvtgvc38sfd9zehtgsp3eh8k269naq949u5qdcqm3x35mjg2uctqfdn3yq" ); + + // Last 20 bytes only, which is 0x1cdcf6568b3e80b52f2806e01b89a34dc90ae616 + let addr = + CosmosAddress::from_h256(key, prefix, 20).expect("Cosmos address creation failed"); + assert_eq!( + addr.address(), + "dual1rnw0v45t86qt2tegqmsphzdrfhys4esk9ktul7" + ); } } diff --git a/rust/chains/hyperlane-cosmos/src/mailbox.rs b/rust/chains/hyperlane-cosmos/src/mailbox.rs index bb94bc7febc..9ff0c06f5b9 100644 --- a/rust/chains/hyperlane-cosmos/src/mailbox.rs +++ b/rust/chains/hyperlane-cosmos/src/mailbox.rs @@ -64,8 +64,12 @@ impl CosmosMailbox { } /// Prefix used in the bech32 address encoding - pub fn prefix(&self) -> String { - self.config.get_prefix() + pub fn bech32_prefix(&self) -> String { + self.config.get_bech32_prefix() + } + + fn contract_address_bytes(&self) -> usize { + self.config.get_contract_address_bytes() } } @@ -151,7 +155,12 @@ impl Mailbox for CosmosMailbox { #[instrument(err, ret, skip(self))] async fn recipient_ism(&self, recipient: H256) -> ChainResult { - let address = CosmosAddress::from_h256(recipient, &self.prefix())?.address(); + let address = CosmosAddress::from_h256( + recipient, + &self.bech32_prefix(), + self.contract_address_bytes(), + )? + .address(); let payload = mailbox::RecipientIsmRequest { recipient_ism: mailbox::RecipientIsmRequestInner { diff --git a/rust/chains/hyperlane-cosmos/src/providers/grpc.rs b/rust/chains/hyperlane-cosmos/src/providers/grpc.rs index 9152dd3d437..3594c7398e8 100644 --- a/rust/chains/hyperlane-cosmos/src/providers/grpc.rs +++ b/rust/chains/hyperlane-cosmos/src/providers/grpc.rs @@ -112,7 +112,13 @@ impl WasmGrpcProvider { Endpoint::new(conf.get_grpc_url()).map_err(Into::::into)?; let channel = endpoint.connect_lazy(); let contract_address = locator - .map(|l| CosmosAddress::from_h256(l.address, &conf.get_prefix())) + .map(|l| { + CosmosAddress::from_h256( + l.address, + &conf.get_bech32_prefix(), + conf.get_contract_address_bytes(), + ) + }) .transpose()?; Ok(Self { diff --git a/rust/chains/hyperlane-cosmos/src/providers/rpc.rs b/rust/chains/hyperlane-cosmos/src/providers/rpc.rs index 1f0d2a24a12..04c4f2f12f2 100644 --- a/rust/chains/hyperlane-cosmos/src/providers/rpc.rs +++ b/rust/chains/hyperlane-cosmos/src/providers/rpc.rs @@ -76,7 +76,8 @@ impl CosmosWasmIndexer { provider, contract_address: CosmosAddress::from_h256( locator.address, - conf.get_prefix().as_str(), + conf.get_bech32_prefix().as_str(), + conf.get_contract_address_bytes(), )?, target_event_kind: format!("{}-{}", Self::WASM_TYPE, event_type), reorg_period, diff --git a/rust/chains/hyperlane-cosmos/src/trait_builder.rs b/rust/chains/hyperlane-cosmos/src/trait_builder.rs index 81c16b7846d..2bacb4d2f55 100644 --- a/rust/chains/hyperlane-cosmos/src/trait_builder.rs +++ b/rust/chains/hyperlane-cosmos/src/trait_builder.rs @@ -12,14 +12,18 @@ pub struct ConnectionConf { rpc_url: String, /// The chain ID chain_id: String, - /// The prefix for the account address - prefix: String, + /// The human readable address prefix for the chains using bech32. + bech32_prefix: String, /// Canoncial Assets Denom canonical_asset: String, /// The gas price set by the cosmos-sdk validator. Note that this represents the /// minimum price set by the validator. /// More details here: https://docs.cosmos.network/main/learn/beginner/gas-fees#antehandler gas_price: RawCosmosAmount, + /// The number of bytes used to represent a contract address. + /// Cosmos address lengths are sometimes less than 32 bytes, so this helps to serialize it in + /// bech32 with the appropriate length. + contract_address_bytes: usize, } /// Untyped cosmos amount @@ -86,9 +90,9 @@ impl ConnectionConf { self.chain_id.clone() } - /// Get the prefix - pub fn get_prefix(&self) -> String { - self.prefix.clone() + /// Get the bech32 prefix + pub fn get_bech32_prefix(&self) -> String { + self.bech32_prefix.clone() } /// Get the asset @@ -101,22 +105,29 @@ impl ConnectionConf { self.gas_price.clone() } + /// Get the number of bytes used to represent a contract address + pub fn get_contract_address_bytes(&self) -> usize { + self.contract_address_bytes + } + /// Create a new connection configuration pub fn new( grpc_url: String, rpc_url: String, chain_id: String, - prefix: String, + bech32_prefix: String, canonical_asset: String, minimum_gas_price: RawCosmosAmount, + contract_address_bytes: usize, ) -> Self { Self { grpc_url, rpc_url, chain_id, - prefix, + bech32_prefix, canonical_asset, gas_price: minimum_gas_price, + contract_address_bytes, } } } diff --git a/rust/config/mainnet3_config.json b/rust/config/mainnet3_config.json index 361e59d0adf..147bb642ef7 100644 --- a/rust/config/mainnet3_config.json +++ b/rust/config/mainnet3_config.json @@ -431,11 +431,12 @@ ], "grpcUrl": "https://grpc-kralum.neutron-1.neutron.org:80", "canonicalAsset": "untrn", - "prefix": "neutron", + "bech32Prefix": "neutron", "gasPrice": { "amount": "0.57", "denom": "untrn" }, + "contractAddressBytes": 32, "index": { "from": 4000000, "chunk": 100000 diff --git a/rust/hyperlane-base/src/settings/parser/connection_parser.rs b/rust/hyperlane-base/src/settings/parser/connection_parser.rs index 55cce2be385..5d42ce44117 100644 --- a/rust/hyperlane-base/src/settings/parser/connection_parser.rs +++ b/rust/hyperlane-base/src/settings/parser/connection_parser.rs @@ -69,11 +69,14 @@ pub fn build_cosmos_connection_conf( let prefix = chain .chain(err) - .get_key("prefix") + .get_key("bech32Prefix") .parse_string() .end() .or_else(|| { - local_err.push(&chain.cwp + "prefix", eyre!("Missing prefix for chain")); + local_err.push( + &chain.cwp + "bech32Prefix", + eyre!("Missing bech32 prefix for chain"), + ); None }); @@ -100,6 +103,12 @@ pub fn build_cosmos_connection_conf( .and_then(parse_cosmos_gas_price) .end(); + let contract_address_bytes = chain + .chain(err) + .get_opt_key("contractAddressBytes") + .parse_u64() + .end(); + if !local_err.is_ok() { err.merge(local_err); None @@ -111,6 +120,7 @@ pub fn build_cosmos_connection_conf( prefix.unwrap().to_string(), canonical_asset.unwrap(), gas_price.unwrap(), + contract_address_bytes.unwrap().try_into().unwrap(), ))) } } diff --git a/rust/utils/run-locally/src/cosmos/deploy.rs b/rust/utils/run-locally/src/cosmos/deploy.rs index 25a8d1dae80..ab02a2bee46 100644 --- a/rust/utils/run-locally/src/cosmos/deploy.rs +++ b/rust/utils/run-locally/src/cosmos/deploy.rs @@ -27,7 +27,7 @@ pub struct IGPOracleInstantiateMsg { #[cw_serde] pub struct EmptyMsg {} -const PREFIX: &str = "osmo"; +const BECH32_PREFIX: &str = "osmo"; #[apply(as_task)] pub fn deploy_cw_hyperlane( @@ -46,7 +46,7 @@ pub fn deploy_cw_hyperlane( codes.hpl_mailbox, core::mailbox::InstantiateMsg { owner: deployer_addr.to_string(), - hrp: PREFIX.to_string(), + hrp: BECH32_PREFIX.to_string(), domain, }, "hpl_mailbox", @@ -68,7 +68,7 @@ pub fn deploy_cw_hyperlane( Some(deployer_addr), codes.hpl_igp, GasOracleInitMsg { - hrp: PREFIX.to_string(), + hrp: BECH32_PREFIX.to_string(), owner: deployer_addr.clone(), gas_token: "uosmo".to_string(), beneficiary: deployer_addr.clone(), @@ -159,7 +159,7 @@ pub fn deploy_cw_hyperlane( Some(deployer_addr), codes.hpl_validator_announce, core::va::InstantiateMsg { - hrp: PREFIX.to_string(), + hrp: BECH32_PREFIX.to_string(), mailbox: mailbox.to_string(), }, "hpl_validator_announce", @@ -173,7 +173,7 @@ pub fn deploy_cw_hyperlane( Some(deployer_addr), codes.hpl_test_mock_msg_receiver, TestMockMsgReceiverInstantiateMsg { - hrp: PREFIX.to_string(), + hrp: BECH32_PREFIX.to_string(), }, "hpl_test_mock_msg_receiver", ); diff --git a/rust/utils/run-locally/src/cosmos/types.rs b/rust/utils/run-locally/src/cosmos/types.rs index 7a157556557..795d12ff39d 100644 --- a/rust/utils/run-locally/src/cosmos/types.rs +++ b/rust/utils/run-locally/src/cosmos/types.rs @@ -119,10 +119,11 @@ pub struct AgentConfig { pub chain_id: String, pub rpc_urls: Vec, pub grpc_url: String, - pub prefix: String, + pub bech32_prefix: String, pub signer: AgentConfigSigner, pub index: AgentConfigIndex, pub gas_price: RawCosmosAmount, + pub contract_address_bytes: usize, } #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] @@ -156,7 +157,7 @@ impl AgentConfig { ), }], grpc_url: format!("http://{}", network.launch_resp.endpoint.grpc_addr), - prefix: "osmo".to_string(), + bech32_prefix: "osmo".to_string(), signer: AgentConfigSigner { typ: "cosmosKey".to_string(), key: format!("0x{}", hex::encode(validator.priv_key.to_bytes())), @@ -166,6 +167,7 @@ impl AgentConfig { denom: "uosmo".to_string(), amount: "0.05".to_string(), }, + contract_address_bytes: 32, index: AgentConfigIndex { from: 1, chunk: 100, diff --git a/solidity/test/message.test.ts b/solidity/test/message.test.ts index f39cb6affca..bdee259fb03 100644 --- a/solidity/test/message.test.ts +++ b/solidity/test/message.test.ts @@ -8,21 +8,26 @@ import { } from '@hyperlane-xyz/utils'; import testCases from '../../vectors/message.json'; -import { TestMessage, TestMessage__factory } from '../types'; +import { Mailbox__factory, TestMessage, TestMessage__factory } from '../types'; const remoteDomain = 1000; const localDomain = 2000; -const version = 0; const nonce = 11; describe('Message', async () => { let messageLib: TestMessage; + let version: number; before(async () => { const [signer] = await ethers.getSigners(); const Message = new TestMessage__factory(signer); messageLib = await Message.deploy(); + + // For consistency with the Mailbox version + const Mailbox = new Mailbox__factory(signer); + const mailbox = await Mailbox.deploy(localDomain); + version = await mailbox.VERSION(); }); it('Returns fields from a message', async () => { diff --git a/typescript/sdk/src/metadata/agentConfig.ts b/typescript/sdk/src/metadata/agentConfig.ts index 5be7f21fcd1..e7133b4c4a1 100644 --- a/typescript/sdk/src/metadata/agentConfig.ts +++ b/typescript/sdk/src/metadata/agentConfig.ts @@ -92,6 +92,30 @@ export type AgentSignerCosmosKey = z.infer; export type AgentSignerNode = z.infer; export type AgentSigner = z.infer; +// Additional chain metadata for Cosmos chains required by the agents. +const AgentCosmosChainMetadataSchema = z.object({ + canonicalAsset: z + .string() + .describe( + 'The name of the canonical asset for this chain, usually in "micro" form, e.g. untrn', + ), + gasPrice: z.object({ + denom: z + .string() + .describe('The coin denom, usually in "micro" form, e.g. untrn'), + amount: z + .string() + .regex(/^(\d*[.])?\d+$/) + .describe('The the gas price, in denom, to pay for each unit of gas'), + }), + contractAddressBytes: z + .number() + .int() + .positive() + .lte(32) + .describe('The number of bytes used to represent a contract address.'), +}); + export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge( HyperlaneDeploymentArtifactsSchema, ) @@ -126,6 +150,7 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge( }) .optional(), }) + .merge(AgentCosmosChainMetadataSchema.partial()) .refine((metadata) => { // Make sure that the signer is valid for the protocol @@ -138,25 +163,47 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge( switch (metadata.protocol) { case ProtocolType.Ethereum: - return [ - AgentSignerKeyType.Hex, - signerType === AgentSignerKeyType.Aws, - signerType === AgentSignerKeyType.Node, - ].includes(signerType); + if ( + ![ + AgentSignerKeyType.Hex, + signerType === AgentSignerKeyType.Aws, + signerType === AgentSignerKeyType.Node, + ].includes(signerType) + ) { + return false; + } + break; case ProtocolType.Cosmos: - return [AgentSignerKeyType.Cosmos].includes(signerType); + if (![AgentSignerKeyType.Cosmos].includes(signerType)) { + return false; + } + break; case ProtocolType.Sealevel: - return [AgentSignerKeyType.Hex].includes(signerType); + if (![AgentSignerKeyType.Hex].includes(signerType)) { + return false; + } + break; case ProtocolType.Fuel: - return [AgentSignerKeyType.Hex].includes(signerType); + if (![AgentSignerKeyType.Hex].includes(signerType)) { + return false; + } + break; default: - // Just default to true if we don't know the protocol - return true; + // Just accept it if we don't know the protocol } + + // If the protocol type is Cosmos, require everything in AgentCosmosChainMetadataSchema + if (metadata.protocol === ProtocolType.Cosmos) { + if (!AgentCosmosChainMetadataSchema.safeParse(metadata).success) { + return false; + } + } + + return true; }); export type AgentChainMetadata = z.infer; @@ -342,6 +389,8 @@ export type ValidatorConfig = z.infer; export type AgentConfig = z.infer; +// Note this works well for EVM chains only, and likely needs some love +// before being useful for non-EVM chains. export function buildAgentConfig( chains: ChainName[], multiProvider: MultiProvider, diff --git a/vectors/message.json b/vectors/message.json index 6a21a198c9d..16644e9360c 100644 --- a/vectors/message.json +++ b/vectors/message.json @@ -1 +1 @@ -[{"body":[18,52],"destination":2000,"id":"0x545b9ae16e93875efda786a09f3b78221d7f568f46a445fe4cd4a1e38096c576","nonce":0,"origin":1000,"recipient":"0x0000000000000000000000002222222222222222222222222222222222222222","sender":"0x0000000000000000000000001111111111111111111111111111111111111111","version":0}] \ No newline at end of file +[{"body":[18,52],"destination":2000,"id":"0xf8a66f8aadee751d842616fee0ed14a3ad6da1e13564920364ee0ad35a02703f","nonce":0,"origin":1000,"recipient":"0x0000000000000000000000002222222222222222222222222222222222222222","sender":"0x0000000000000000000000001111111111111111111111111111111111111111","version":3}] \ No newline at end of file