Support cosmos contract byte lengths other than 32 (#3147)

### Description

fixes #3143 

Initially went down a path that would use protocol-specific types for
addresses as suggested by #3143. I had made a `HyperlaneConnectionConf`
enum with protocol-specific variants whose values were `addresses:
CoreContractAddresses<ProtocolSpecificAddressType>` and `connection:
ProtocolSpecificConnectionConf`. This worked pretty well until I hit the
ISM logic.

Because hyperlane-core is where the Mailbox trait is defined, the return
type of `recipient_ism` in the trait cannot involve protocol specific
types. It can't be moved to hyperlane-base because hyperlane-base
imports the chain crates, so we'd have a cyclic dependency. I
experimented with moving away from H256 to something like a Vec<u8> or
string, but this felt a bit weird.

In the end we decided to keep H256s as the global representation for
contract addresses for now, with the intent of eventually changing this,
and to support the varying length situation in a cosmos config

### Drive-by changes

- Added some cosmos specific agent configurations into the sdk
- Moved to bech32_prefix in the agents for consistency with what the
SDK's chain metadata already does
- I guess no one's ran cargo test in a while so vectors/message.json got
a new v3 message lol

### Related issues

Fixes #3143 

### Backward compatibility

Changes prefix to bech32_prefix in the agent config, and now requires
`contractAddressBytes`

### Testing

Tested merged with #3144 and all worked

---------

Co-authored-by: Daniel Savu <23065004+daniel-savu@users.noreply.github.com>
pull/3180/head
Trevor Porter 10 months ago committed by GitHub
parent 528a19022b
commit 0cad4f7407
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 39
      rust/chains/hyperlane-cosmos/src/libs/address.rs
  2. 15
      rust/chains/hyperlane-cosmos/src/mailbox.rs
  3. 8
      rust/chains/hyperlane-cosmos/src/providers/grpc.rs
  4. 3
      rust/chains/hyperlane-cosmos/src/providers/rpc.rs
  5. 25
      rust/chains/hyperlane-cosmos/src/trait_builder.rs
  6. 3
      rust/config/mainnet3_config.json
  7. 14
      rust/hyperlane-base/src/settings/parser/connection_parser.rs
  8. 10
      rust/utils/run-locally/src/cosmos/deploy.rs
  9. 6
      rust/utils/run-locally/src/cosmos/types.rs
  10. 9
      solidity/test/message.test.ts
  11. 63
      typescript/sdk/src/metadata/agentConfig.ts
  12. 2
      vectors/message.json

@ -48,9 +48,21 @@ impl CosmosAddress {
/// ///
/// - digest: H256 digest (hex representation of address) /// - digest: H256 digest (hex representation of address)
/// - prefix: Bech32 prefix /// - prefix: Bech32 prefix
pub fn from_h256(digest: H256, prefix: &str) -> ChainResult<Self> { /// - 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<Self> {
// This is the hex-encoded version of the address // 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 // Bech32 encode it
let account_id = let account_id =
AccountId::new(prefix, bytes).map_err(Into::<HyperlaneCosmosError>::into)?; AccountId::new(prefix, bytes).map_err(Into::<HyperlaneCosmosError>::into)?;
@ -132,11 +144,13 @@ pub mod test {
addr.address(), addr.address(),
"neutron1kknekjxg0ear00dky5ykzs8wwp2gz62z9s6aaj" "neutron1kknekjxg0ear00dky5ykzs8wwp2gz62z9s6aaj"
); );
// TODO: watch out for this edge case. This check will fail unless
// the first 12 bytes are removed from the digest. // Create an address with the same digest & explicitly set the byte count to 20,
// let digest = addr.digest(); // which should have the same result as the above.
// let addr2 = CosmosAddress::from_h256(digest, prefix).expect("Cosmos address creation failed"); let digest = addr.digest();
// assert_eq!(addr.address(), addr2.address()); let addr2 =
CosmosAddress::from_h256(digest, prefix, 20).expect("Cosmos address creation failed");
assert_eq!(addr.address(), addr2.address());
} }
#[test] #[test]
@ -144,10 +158,19 @@ pub mod test {
let hex_key = "0x1b16866227825a5166eb44031cdcf6568b3e80b52f2806e01b89a34dc90ae616"; let hex_key = "0x1b16866227825a5166eb44031cdcf6568b3e80b52f2806e01b89a34dc90ae616";
let key = hex_or_base58_to_h256(hex_key).unwrap(); let key = hex_or_base58_to_h256(hex_key).unwrap();
let prefix = "dual"; 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!( assert_eq!(
addr.address(), addr.address(),
"dual1rvtgvc38sfd9zehtgsp3eh8k269naq949u5qdcqm3x35mjg2uctqfdn3yq" "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"
);
} }
} }

@ -64,8 +64,12 @@ impl CosmosMailbox {
} }
/// Prefix used in the bech32 address encoding /// Prefix used in the bech32 address encoding
pub fn prefix(&self) -> String { pub fn bech32_prefix(&self) -> String {
self.config.get_prefix() 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))] #[instrument(err, ret, skip(self))]
async fn recipient_ism(&self, recipient: H256) -> ChainResult<H256> { async fn recipient_ism(&self, recipient: H256) -> ChainResult<H256> {
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 { let payload = mailbox::RecipientIsmRequest {
recipient_ism: mailbox::RecipientIsmRequestInner { recipient_ism: mailbox::RecipientIsmRequestInner {

@ -112,7 +112,13 @@ impl WasmGrpcProvider {
Endpoint::new(conf.get_grpc_url()).map_err(Into::<HyperlaneCosmosError>::into)?; Endpoint::new(conf.get_grpc_url()).map_err(Into::<HyperlaneCosmosError>::into)?;
let channel = endpoint.connect_lazy(); let channel = endpoint.connect_lazy();
let contract_address = locator 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()?; .transpose()?;
Ok(Self { Ok(Self {

@ -76,7 +76,8 @@ impl CosmosWasmIndexer {
provider, provider,
contract_address: CosmosAddress::from_h256( contract_address: CosmosAddress::from_h256(
locator.address, 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), target_event_kind: format!("{}-{}", Self::WASM_TYPE, event_type),
reorg_period, reorg_period,

@ -12,14 +12,18 @@ pub struct ConnectionConf {
rpc_url: String, rpc_url: String,
/// The chain ID /// The chain ID
chain_id: String, chain_id: String,
/// The prefix for the account address /// The human readable address prefix for the chains using bech32.
prefix: String, bech32_prefix: String,
/// Canoncial Assets Denom /// Canoncial Assets Denom
canonical_asset: String, canonical_asset: String,
/// The gas price set by the cosmos-sdk validator. Note that this represents the /// The gas price set by the cosmos-sdk validator. Note that this represents the
/// minimum price set by the validator. /// minimum price set by the validator.
/// More details here: https://docs.cosmos.network/main/learn/beginner/gas-fees#antehandler /// More details here: https://docs.cosmos.network/main/learn/beginner/gas-fees#antehandler
gas_price: RawCosmosAmount, 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 /// Untyped cosmos amount
@ -86,9 +90,9 @@ impl ConnectionConf {
self.chain_id.clone() self.chain_id.clone()
} }
/// Get the prefix /// Get the bech32 prefix
pub fn get_prefix(&self) -> String { pub fn get_bech32_prefix(&self) -> String {
self.prefix.clone() self.bech32_prefix.clone()
} }
/// Get the asset /// Get the asset
@ -101,22 +105,29 @@ impl ConnectionConf {
self.gas_price.clone() 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 /// Create a new connection configuration
pub fn new( pub fn new(
grpc_url: String, grpc_url: String,
rpc_url: String, rpc_url: String,
chain_id: String, chain_id: String,
prefix: String, bech32_prefix: String,
canonical_asset: String, canonical_asset: String,
minimum_gas_price: RawCosmosAmount, minimum_gas_price: RawCosmosAmount,
contract_address_bytes: usize,
) -> Self { ) -> Self {
Self { Self {
grpc_url, grpc_url,
rpc_url, rpc_url,
chain_id, chain_id,
prefix, bech32_prefix,
canonical_asset, canonical_asset,
gas_price: minimum_gas_price, gas_price: minimum_gas_price,
contract_address_bytes,
} }
} }
} }

@ -431,11 +431,12 @@
], ],
"grpcUrl": "https://grpc-kralum.neutron-1.neutron.org:80", "grpcUrl": "https://grpc-kralum.neutron-1.neutron.org:80",
"canonicalAsset": "untrn", "canonicalAsset": "untrn",
"prefix": "neutron", "bech32Prefix": "neutron",
"gasPrice": { "gasPrice": {
"amount": "0.57", "amount": "0.57",
"denom": "untrn" "denom": "untrn"
}, },
"contractAddressBytes": 32,
"index": { "index": {
"from": 4000000, "from": 4000000,
"chunk": 100000 "chunk": 100000

@ -69,11 +69,14 @@ pub fn build_cosmos_connection_conf(
let prefix = chain let prefix = chain
.chain(err) .chain(err)
.get_key("prefix") .get_key("bech32Prefix")
.parse_string() .parse_string()
.end() .end()
.or_else(|| { .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 None
}); });
@ -100,6 +103,12 @@ pub fn build_cosmos_connection_conf(
.and_then(parse_cosmos_gas_price) .and_then(parse_cosmos_gas_price)
.end(); .end();
let contract_address_bytes = chain
.chain(err)
.get_opt_key("contractAddressBytes")
.parse_u64()
.end();
if !local_err.is_ok() { if !local_err.is_ok() {
err.merge(local_err); err.merge(local_err);
None None
@ -111,6 +120,7 @@ pub fn build_cosmos_connection_conf(
prefix.unwrap().to_string(), prefix.unwrap().to_string(),
canonical_asset.unwrap(), canonical_asset.unwrap(),
gas_price.unwrap(), gas_price.unwrap(),
contract_address_bytes.unwrap().try_into().unwrap(),
))) )))
} }
} }

@ -27,7 +27,7 @@ pub struct IGPOracleInstantiateMsg {
#[cw_serde] #[cw_serde]
pub struct EmptyMsg {} pub struct EmptyMsg {}
const PREFIX: &str = "osmo"; const BECH32_PREFIX: &str = "osmo";
#[apply(as_task)] #[apply(as_task)]
pub fn deploy_cw_hyperlane( pub fn deploy_cw_hyperlane(
@ -46,7 +46,7 @@ pub fn deploy_cw_hyperlane(
codes.hpl_mailbox, codes.hpl_mailbox,
core::mailbox::InstantiateMsg { core::mailbox::InstantiateMsg {
owner: deployer_addr.to_string(), owner: deployer_addr.to_string(),
hrp: PREFIX.to_string(), hrp: BECH32_PREFIX.to_string(),
domain, domain,
}, },
"hpl_mailbox", "hpl_mailbox",
@ -68,7 +68,7 @@ pub fn deploy_cw_hyperlane(
Some(deployer_addr), Some(deployer_addr),
codes.hpl_igp, codes.hpl_igp,
GasOracleInitMsg { GasOracleInitMsg {
hrp: PREFIX.to_string(), hrp: BECH32_PREFIX.to_string(),
owner: deployer_addr.clone(), owner: deployer_addr.clone(),
gas_token: "uosmo".to_string(), gas_token: "uosmo".to_string(),
beneficiary: deployer_addr.clone(), beneficiary: deployer_addr.clone(),
@ -159,7 +159,7 @@ pub fn deploy_cw_hyperlane(
Some(deployer_addr), Some(deployer_addr),
codes.hpl_validator_announce, codes.hpl_validator_announce,
core::va::InstantiateMsg { core::va::InstantiateMsg {
hrp: PREFIX.to_string(), hrp: BECH32_PREFIX.to_string(),
mailbox: mailbox.to_string(), mailbox: mailbox.to_string(),
}, },
"hpl_validator_announce", "hpl_validator_announce",
@ -173,7 +173,7 @@ pub fn deploy_cw_hyperlane(
Some(deployer_addr), Some(deployer_addr),
codes.hpl_test_mock_msg_receiver, codes.hpl_test_mock_msg_receiver,
TestMockMsgReceiverInstantiateMsg { TestMockMsgReceiverInstantiateMsg {
hrp: PREFIX.to_string(), hrp: BECH32_PREFIX.to_string(),
}, },
"hpl_test_mock_msg_receiver", "hpl_test_mock_msg_receiver",
); );

@ -119,10 +119,11 @@ pub struct AgentConfig {
pub chain_id: String, pub chain_id: String,
pub rpc_urls: Vec<AgentUrl>, pub rpc_urls: Vec<AgentUrl>,
pub grpc_url: String, pub grpc_url: String,
pub prefix: String, pub bech32_prefix: String,
pub signer: AgentConfigSigner, pub signer: AgentConfigSigner,
pub index: AgentConfigIndex, pub index: AgentConfigIndex,
pub gas_price: RawCosmosAmount, pub gas_price: RawCosmosAmount,
pub contract_address_bytes: usize,
} }
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
@ -156,7 +157,7 @@ impl AgentConfig {
), ),
}], }],
grpc_url: format!("http://{}", network.launch_resp.endpoint.grpc_addr), grpc_url: format!("http://{}", network.launch_resp.endpoint.grpc_addr),
prefix: "osmo".to_string(), bech32_prefix: "osmo".to_string(),
signer: AgentConfigSigner { signer: AgentConfigSigner {
typ: "cosmosKey".to_string(), typ: "cosmosKey".to_string(),
key: format!("0x{}", hex::encode(validator.priv_key.to_bytes())), key: format!("0x{}", hex::encode(validator.priv_key.to_bytes())),
@ -166,6 +167,7 @@ impl AgentConfig {
denom: "uosmo".to_string(), denom: "uosmo".to_string(),
amount: "0.05".to_string(), amount: "0.05".to_string(),
}, },
contract_address_bytes: 32,
index: AgentConfigIndex { index: AgentConfigIndex {
from: 1, from: 1,
chunk: 100, chunk: 100,

@ -8,21 +8,26 @@ import {
} from '@hyperlane-xyz/utils'; } from '@hyperlane-xyz/utils';
import testCases from '../../vectors/message.json'; import testCases from '../../vectors/message.json';
import { TestMessage, TestMessage__factory } from '../types'; import { Mailbox__factory, TestMessage, TestMessage__factory } from '../types';
const remoteDomain = 1000; const remoteDomain = 1000;
const localDomain = 2000; const localDomain = 2000;
const version = 0;
const nonce = 11; const nonce = 11;
describe('Message', async () => { describe('Message', async () => {
let messageLib: TestMessage; let messageLib: TestMessage;
let version: number;
before(async () => { before(async () => {
const [signer] = await ethers.getSigners(); const [signer] = await ethers.getSigners();
const Message = new TestMessage__factory(signer); const Message = new TestMessage__factory(signer);
messageLib = await Message.deploy(); 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 () => { it('Returns fields from a message', async () => {

@ -92,6 +92,30 @@ export type AgentSignerCosmosKey = z.infer<typeof AgentSignerNodeSchema>;
export type AgentSignerNode = z.infer<typeof AgentSignerNodeSchema>; export type AgentSignerNode = z.infer<typeof AgentSignerNodeSchema>;
export type AgentSigner = z.infer<typeof AgentSignerSchema>; export type AgentSigner = z.infer<typeof AgentSignerSchema>;
// 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( export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge(
HyperlaneDeploymentArtifactsSchema, HyperlaneDeploymentArtifactsSchema,
) )
@ -126,6 +150,7 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge(
}) })
.optional(), .optional(),
}) })
.merge(AgentCosmosChainMetadataSchema.partial())
.refine((metadata) => { .refine((metadata) => {
// Make sure that the signer is valid for the protocol // Make sure that the signer is valid for the protocol
@ -138,25 +163,47 @@ export const AgentChainMetadataSchema = ChainMetadataSchemaObject.merge(
switch (metadata.protocol) { switch (metadata.protocol) {
case ProtocolType.Ethereum: case ProtocolType.Ethereum:
return [ if (
![
AgentSignerKeyType.Hex, AgentSignerKeyType.Hex,
signerType === AgentSignerKeyType.Aws, signerType === AgentSignerKeyType.Aws,
signerType === AgentSignerKeyType.Node, signerType === AgentSignerKeyType.Node,
].includes(signerType); ].includes(signerType)
) {
return false;
}
break;
case ProtocolType.Cosmos: case ProtocolType.Cosmos:
return [AgentSignerKeyType.Cosmos].includes(signerType); if (![AgentSignerKeyType.Cosmos].includes(signerType)) {
return false;
}
break;
case ProtocolType.Sealevel: case ProtocolType.Sealevel:
return [AgentSignerKeyType.Hex].includes(signerType); if (![AgentSignerKeyType.Hex].includes(signerType)) {
return false;
}
break;
case ProtocolType.Fuel: case ProtocolType.Fuel:
return [AgentSignerKeyType.Hex].includes(signerType); if (![AgentSignerKeyType.Hex].includes(signerType)) {
return false;
}
break;
default: default:
// Just default to true if we don't know the protocol // Just accept it if we don't know the protocol
return true;
} }
// 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<typeof AgentChainMetadataSchema>; export type AgentChainMetadata = z.infer<typeof AgentChainMetadataSchema>;
@ -342,6 +389,8 @@ export type ValidatorConfig = z.infer<typeof ValidatorAgentConfigSchema>;
export type AgentConfig = z.infer<typeof AgentConfigSchema>; export type AgentConfig = z.infer<typeof AgentConfigSchema>;
// Note this works well for EVM chains only, and likely needs some love
// before being useful for non-EVM chains.
export function buildAgentConfig( export function buildAgentConfig(
chains: ChainName[], chains: ChainName[],
multiProvider: MultiProvider, multiProvider: MultiProvider,

@ -1 +1 @@
[{"body":[18,52],"destination":2000,"id":"0x545b9ae16e93875efda786a09f3b78221d7f568f46a445fe4cd4a1e38096c576","nonce":0,"origin":1000,"recipient":"0x0000000000000000000000002222222222222222222222222222222222222222","sender":"0x0000000000000000000000001111111111111111111111111111111111111111","version":0}] [{"body":[18,52],"destination":2000,"id":"0xf8a66f8aadee751d842616fee0ed14a3ad6da1e13564920364ee0ad35a02703f","nonce":0,"origin":1000,"recipient":"0x0000000000000000000000002222222222222222222222222222222222222222","sender":"0x0000000000000000000000001111111111111111111111111111111111111111","version":3}]
Loading…
Cancel
Save