From ece3349d28519787d84463ef6fa18c90fb57a528 Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 16 Feb 2024 04:34:13 +0800 Subject: [PATCH] refactor(connection-router-api): replace the code with the latest (#272) --- Cargo.lock | 6 + packages/connection-router-api/Cargo.toml | 8 + packages/connection-router-api/src/error.rs | 16 +- packages/connection-router-api/src/lib.rs | 4 +- packages/connection-router-api/src/msg.rs | 8 +- .../connection-router-api/src/primitives.rs | 420 +++++++++++++----- packages/gateway-api/src/msg.rs | 2 +- 7 files changed, 347 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 536cda712..2b9f307e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,16 +1493,22 @@ name = "connection-router-api" version = "0.1.0" dependencies = [ "axelar-wasm-std", + "axelar-wasm-std-derive", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.1.0", "error-stack", "flagset", + "hex", + "rand", "regex", + "report", "schemars", "serde", "serde_json", + "sha3 0.10.8", "thiserror", + "valuable", ] [[package]] diff --git a/packages/connection-router-api/Cargo.toml b/packages/connection-router-api/Cargo.toml index dcdf4b499..a57eb794f 100644 --- a/packages/connection-router-api/Cargo.toml +++ b/packages/connection-router-api/Cargo.toml @@ -7,13 +7,21 @@ edition = "2021" [dependencies] axelar-wasm-std = { workspace = true } +axelar-wasm-std-derive = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-storage-plus = { workspace = true } error-stack = { workspace = true } flagset = { version = "0.4.3", features = ["serde"] } regex = "1.10.0" +report = { workspace = true } schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +sha3 = { workspace = true } thiserror = { workspace = true } +valuable = "0.1.0" + +[dev-dependencies] +hex = "0.4.3" +rand = "0.8.5" diff --git a/packages/connection-router-api/src/error.rs b/packages/connection-router-api/src/error.rs index dd0c3c869..f179a6a6b 100644 --- a/packages/connection-router-api/src/error.rs +++ b/packages/connection-router-api/src/error.rs @@ -1,14 +1,20 @@ use thiserror::Error; +use axelar_wasm_std_derive::IntoContractError; + /// A chain name must adhere to the following rules: /// 1. it can optionally start with an uppercase letter, followed by one or more lowercase letters /// 2. it can have an optional suffix of an optional dash and one or more digits ("1", "03", "-5" are all valid suffixes) pub const CHAIN_NAME_REGEX: &str = "^[A-Z]?[a-z]+(-?[0-9]+)?$"; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq, IntoContractError)] pub enum Error { - #[error("chain name '{}' must adhere to the pattern '{}'", .0, CHAIN_NAME_REGEX)] - ChainNamePatternMismatch(String), - #[error("address must not be empty")] - EmptyAddress, + #[error("address is invalid")] + InvalidAddress, + + #[error("message ID is invalid")] + InvalidMessageId, + + #[error("chain name is invalid")] + InvalidChainName, } diff --git a/packages/connection-router-api/src/lib.rs b/packages/connection-router-api/src/lib.rs index d01006aac..50248ad6d 100644 --- a/packages/connection-router-api/src/lib.rs +++ b/packages/connection-router-api/src/lib.rs @@ -1,3 +1,5 @@ +mod primitives; + pub mod error; pub mod msg; -pub mod primitives; +pub use primitives::*; diff --git a/packages/connection-router-api/src/msg.rs b/packages/connection-router-api/src/msg.rs index 3a7def309..bcccbbb68 100644 --- a/packages/connection-router-api/src/msg.rs +++ b/packages/connection-router-api/src/msg.rs @@ -1,6 +1,7 @@ -use crate::primitives::*; use cosmwasm_schema::{cw_serde, QueryResponses}; +use crate::primitives::*; + #[cw_serde] pub enum ExecuteMsg { /* @@ -44,4 +45,7 @@ pub enum ExecuteMsg { #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg {} +pub enum QueryMsg { + #[returns(ChainEndpoint)] + GetChainInfo(ChainName), +} diff --git a/packages/connection-router-api/src/primitives.rs b/packages/connection-router-api/src/primitives.rs index a44e0882f..ffa645d43 100644 --- a/packages/connection-router-api/src/primitives.rs +++ b/packages/connection-router-api/src/primitives.rs @@ -1,73 +1,178 @@ -use crate::error::*; -use axelar_wasm_std::nonempty; +use std::{any::type_name, fmt, ops::Deref, str::FromStr}; + +use axelar_wasm_std::flagset::FlagSet; +use axelar_wasm_std::{hash::Hash, nonempty, FnExt}; use cosmwasm_schema::cw_serde; -use cosmwasm_schema::serde::{Deserialize, Serialize}; -use error_stack::{Report, ResultExt}; +use cosmwasm_std::{Addr, Attribute, HexBinary}; +use cosmwasm_std::{StdError, StdResult}; +use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey}; +use error_stack::Report; +use error_stack::ResultExt; use flagset::flags; -use regex::Regex; use schemars::JsonSchema; -use std::fmt; -use std::fmt::{Display, Formatter}; -use std::hash::Hash; -use std::ops::Deref; -use std::str::FromStr; +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Keccak256}; +use valuable::Valuable; + +use crate::error::*; + +pub const ID_SEPARATOR: char = ':'; #[cw_serde] pub struct Message { - /// This field can hold arbitrary data. It has only two requirements: - /// 1. it must be possible to use its content to find the corresponding message on the source chain - /// 2. the ID must uniquely identify the message on the source chain, i.e. no two messages can have the same ID, and no single message can have multiple valid IDs. - /// - /// IMPORTANT: Verifier contracts must enforce these requirements. - pub id: String, - pub source_chain: ChainName, + pub cc_id: CrossChainId, pub source_address: Address, pub destination_chain: ChainName, pub destination_address: Address, - /// hash length is enforced to be 32 bytes + /// for better user experience, the payload hash gets encoded into hex at the edges (input/output), + /// but internally, we treat it as raw bytes to enforce it's format. + #[serde(with = "axelar_wasm_std::hex")] + #[schemars(with = "String")] // necessary attribute in conjunction with #[serde(with ...)] pub payload_hash: [u8; 32], } -// [cw_serde] has been expanded here because we need to implement PartialEq manually -#[derive( - ::cosmwasm_schema::serde::Serialize, - ::cosmwasm_schema::serde::Deserialize, - Clone, - Debug, - ::cosmwasm_schema::schemars::JsonSchema, -)] -#[serde(deny_unknown_fields, crate = "::cosmwasm_schema::serde")] -#[schemars(crate = "::cosmwasm_schema::schemars")] -#[serde(try_from = "String")] -pub struct ChainName(String); +impl Message { + pub fn hash(&self) -> Hash { + let mut hasher = Keccak256::new(); + hasher.update(self.cc_id.to_string()); + hasher.update(self.source_address.as_str()); + hasher.update(self.destination_chain.as_ref()); + hasher.update(self.destination_address.as_str()); + hasher.update(self.payload_hash); + hasher.finalize().into() + } +} + +impl From for Vec { + fn from(other: Message) -> Self { + vec![ + ("id", other.cc_id.id).into(), + ("source_chain", other.cc_id.chain).into(), + ("source_addresses", other.source_address.deref()).into(), + ("destination_chain", other.destination_chain).into(), + ("destination_addresses", other.destination_address.deref()).into(), + ( + "payload_hash", + HexBinary::from(other.payload_hash).to_string(), + ) + .into(), + ] + } +} + +#[cw_serde] +pub struct Address(nonempty::String); + +impl Deref for Address { + type Target = String; -impl Hash for ChainName { - /// this is implemented manually because we want to ignore case when hashing - fn hash(&self, state: &mut H) { - self.0.to_lowercase().hash(state) + fn deref(&self) -> &Self::Target { + self.0.deref() } } -impl PartialEq for ChainName { - /// this is implemented manually because we want to ignore case when checking equality - fn eq(&self, other: &Self) -> bool { - self.0.to_lowercase() == other.0.to_lowercase() +impl FromStr for Address { + type Err = Report; + + fn from_str(s: &str) -> Result { + Address::try_from(s.to_string()) } } -impl FromStr for ChainName { +impl TryFrom for Address { + type Error = Report; + + fn try_from(value: String) -> Result { + Ok(Address( + value + .parse::() + .change_context(Error::InvalidAddress)?, + )) + } +} + +#[cw_serde] +#[derive(Eq, Hash)] +pub struct CrossChainId { + pub chain: ChainName, + pub id: nonempty::String, +} + +/// todo: remove this when state::NewMessage is used +impl FromStr for CrossChainId { type Err = Error; - fn from_str(chain_name: &str) -> Result { - let is_chain_name_valid = Regex::new(CHAIN_NAME_REGEX) - .expect("invalid regex pattern for chain name") - .is_match(chain_name); + fn from_str(s: &str) -> Result { + let parts = s.split_once(ID_SEPARATOR); + let (chain, id) = parts + .map(|(chain, id)| { + ( + chain.parse::(), + id.parse::() + .map_err(|_| Error::InvalidMessageId), + ) + }) + .ok_or(Error::InvalidMessageId)?; + Ok(CrossChainId { + chain: chain?, + id: id?, + }) + } +} + +impl fmt::Display for CrossChainId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}{}", self.chain, ID_SEPARATOR, *self.id) + } +} + +impl PrimaryKey<'_> for CrossChainId { + type Prefix = ChainName; + type SubPrefix = (); + type Suffix = String; + type SuperSuffix = (ChainName, String); + + fn key(&self) -> Vec { + let mut keys = self.chain.key(); + keys.extend(self.id.key()); + keys + } +} + +impl KeyDeserialize for CrossChainId { + type Output = Self; + + fn from_vec(value: Vec) -> StdResult { + let (chain, id) = <(ChainName, String)>::from_vec(value)?; + Ok(CrossChainId { + chain, + id: id + .try_into() + .map_err(|err| StdError::parse_err(type_name::(), err))?, + }) + } +} + +#[cw_serde] +#[serde(try_from = "String")] +#[derive(Eq, Hash, Valuable)] +pub struct ChainName(String); - if is_chain_name_valid { - Ok(ChainName(chain_name.to_string())) - } else { - Err(Error::ChainNamePatternMismatch(chain_name.to_string())) +impl FromStr for ChainName { + type Err = Error; + + fn from_str(s: &str) -> Result { + if s.contains(ID_SEPARATOR) || s.is_empty() { + return Err(Error::InvalidChainName); } + + Ok(ChainName(s.to_lowercase())) + } +} + +impl From for String { + fn from(d: ChainName) -> Self { + d.0 } } @@ -79,36 +184,59 @@ impl TryFrom for ChainName { } } -impl Display for ChainName { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(&self.0, f) +impl fmt::Display for ChainName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) } } -#[cw_serde] -pub struct Address(nonempty::String); +impl PartialEq for ChainName { + fn eq(&self, other: &String) -> bool { + self.0 == other.to_lowercase() + } +} -impl FromStr for Address { - type Err = Report; +impl<'a> PrimaryKey<'a> for ChainName { + type Prefix = (); + type SubPrefix = (); + type Suffix = Self; + type SuperSuffix = Self; - fn from_str(s: &str) -> Result { - s.parse::() - .change_context(Error::EmptyAddress) - .map(Address) + fn key(&self) -> Vec { + vec![Key::Ref(self.0.as_bytes())] } } -impl TryFrom for Address { - type Error = Report; - fn try_from(value: String) -> Result { - value.parse() + +impl<'a> Prefixer<'a> for ChainName { + fn prefix(&self) -> Vec { + vec![Key::Ref(self.0.as_bytes())] } } -impl Deref for Address { - type Target = str; +impl KeyDeserialize for ChainName { + type Output = Self; - fn deref(&self) -> &Self::Target { - self.0.deref() + #[inline(always)] + fn from_vec(value: Vec) -> StdResult { + String::from_utf8(value) + .map_err(StdError::invalid_utf8)? + .then(ChainName::try_from) + .map_err(StdError::invalid_utf8) + } +} + +impl KeyDeserialize for &ChainName { + type Output = ChainName; + + #[inline(always)] + fn from_vec(value: Vec) -> StdResult { + ChainName::from_vec(value) + } +} + +impl AsRef for ChainName { + fn as_ref(&self) -> &str { + self.0.as_str() } } @@ -123,63 +251,139 @@ flags! { } } +#[cw_serde] +pub struct Gateway { + pub address: Addr, +} + +#[cw_serde] +pub struct ChainEndpoint { + pub name: ChainName, + pub gateway: Gateway, + pub frozen_status: FlagSet, +} + +impl ChainEndpoint { + pub fn incoming_frozen(&self) -> bool { + self.frozen_status.contains(GatewayDirection::Incoming) + } + + pub fn outgoing_frozen(&self) -> bool { + self.frozen_status.contains(GatewayDirection::Outgoing) + } +} + #[cfg(test)] mod tests { use super::*; - use std::collections::hash_map::DefaultHasher; - use std::hash::Hasher; + + use cosmwasm_std::to_vec; + use rand::distributions::Alphanumeric; + use rand::{thread_rng, Rng}; + use sha3::{Digest, Sha3_256}; #[test] - fn chain_names_adhere_to_naming_scheme() { - let test_cases = vec![ - ("Ethereum", true), - ("ethereum", true), - ("a", true), - ("terra2", true), - ("terra-2", true), - ("", false), - ("ETHEREUM", false), - ("ethereuM", false), - ("e2e", false), - ("e:e", false), - ("polygon-0-1", false), - ]; - - test_cases.into_iter().for_each(|(name, is_match)| { - assert_eq!( - name.parse::().is_ok(), - is_match, - "mismatch for {}", - name - ); - }); + // Any modifications to the Message struct fields or their types + // will cause this test to fail, indicating that a migration is needed. + fn test_message_struct_unchanged() { + let expected_message_hash = + "9f9b9c55ccf5ce5a82f66385cae9e84e402a272fece5a2e22a199dbefc91d8bf"; + + let msg = dummy_message(); + + assert_eq!( + hex::encode(Sha3_256::digest(&to_vec(&msg).unwrap())), + expected_message_hash + ); } + // If this test fails, it means the message hash has changed and therefore a migration is needed. #[test] - fn chain_name_equality_is_case_insensitive() { - let chain_name_1 = "Ethereum".parse::().unwrap(); - let chain_name_2 = "ethereum".parse::().unwrap(); - assert_eq!(chain_name_1, chain_name_2); + fn hash_id_unchanged() { + let expected_message_hash = + "0135c407f6a58fdcfb879f8d9eae19f870a89f8619537dcde265b4599361a7b6"; + + let msg = dummy_message(); + + assert_eq!(hex::encode(msg.hash()), expected_message_hash); } #[test] - fn chain_name_hash_is_case_insensitive() { - let mut hasher_1 = DefaultHasher::new(); - let chain_name_1 = "Ethereum".parse::().unwrap(); - chain_name_1.hash(&mut hasher_1); - let hash_1 = hasher_1.finish(); + fn should_fail_to_parse_invalid_chain_name() { + // empty + assert_eq!( + "".parse::().unwrap_err(), + Error::InvalidChainName + ); - let mut hasher_2 = DefaultHasher::new(); - let chain_name_2 = "ethereum".parse::().unwrap(); - chain_name_2.hash(&mut hasher_2); - let hash_2 = hasher_2.finish(); + // name contains id separator + assert_eq!( + format!("chain {ID_SEPARATOR}") + .parse::() + .unwrap_err(), + Error::InvalidChainName + ); + } + + #[test] + fn should_parse_to_case_insensitive_chain_name() { + let rand_str: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); + + let chain_name: ChainName = rand_str.parse().unwrap(); - assert_eq!(hash_1, hash_2); + assert_eq!( + chain_name, + rand_str.to_lowercase().parse::().unwrap() + ); + assert_eq!( + chain_name, + rand_str.to_uppercase().parse::().unwrap() + ); } #[test] - fn address_cannot_be_empty() { - assert!("".parse::
().is_err()); - assert!("some_address".parse::
().is_ok()); + fn should_not_deserialize_invalid_chain_name() { + assert_eq!( + "chain name is invalid", + serde_json::from_str::(format!("\"\"").as_str()) + .unwrap_err() + .to_string() + ); + + assert_eq!( + "chain name is invalid", + serde_json::from_str::(format!("\"chain{ID_SEPARATOR}\"").as_str()) + .unwrap_err() + .to_string() + ); + } + + #[test] + fn chain_name_case_insensitive_comparison() { + let chain_name = ChainName::from_str("ethereum").unwrap(); + + assert!(chain_name.eq(&"Ethereum".to_string())); + assert!(chain_name.eq(&"ETHEREUM".to_string())); + assert!(chain_name.eq(&"ethereum".to_string())); + assert!(chain_name.eq(&"ethEReum".to_string())); + + assert!(!chain_name.eq(&"Ethereum-1".to_string())); + } + + fn dummy_message() -> Message { + Message { + cc_id: CrossChainId { + id: "hash:index".parse().unwrap(), + chain: "chain".parse().unwrap(), + }, + source_address: "source_address".parse().unwrap(), + destination_chain: "destination_chain".parse().unwrap(), + destination_address: "destination_address".parse().unwrap(), + payload_hash: [1; 32].into(), + } } } diff --git a/packages/gateway-api/src/msg.rs b/packages/gateway-api/src/msg.rs index d2aacd297..673c1c756 100644 --- a/packages/gateway-api/src/msg.rs +++ b/packages/gateway-api/src/msg.rs @@ -1,4 +1,4 @@ -use connection_router_api::primitives::Message; +use connection_router_api::Message; use cosmwasm_schema::cw_serde; #[cw_serde]