From 29162cc9c57cc39aa2c08f965f827fb740364135 Mon Sep 17 00:00:00 2001 From: Stoyan Kirov Date: Sat, 14 Dec 2024 11:46:38 +0200 Subject: [PATCH] feat: starknet message_id and address format --- Cargo.toml | 31 +- contracts/voting-verifier/Cargo.toml | 6 +- contracts/voting-verifier/src/contract.rs | 76 ++++- contracts/voting-verifier/src/events.rs | 14 +- packages/axelar-wasm-std/Cargo.toml | 14 +- packages/axelar-wasm-std/src/address.rs | 136 +++++++++ packages/axelar-wasm-std/src/msg_id/mod.rs | 8 + .../starknet_field_element_event_index.rs | 284 ++++++++++++++++++ packages/axelar-wasm-std/src/utils.rs | 19 ++ 9 files changed, 566 insertions(+), 22 deletions(-) create mode 100644 packages/axelar-wasm-std/src/msg_id/starknet_field_element_event_index.rs diff --git a/Cargo.toml b/Cargo.toml index ff9bd4504..1066e5de2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] members = [ - "ampd", - "contracts/*", - "external-gateways/*", - "integration-tests", - "packages/*" + "ampd", + "contracts/*", + "external-gateways/*", + "integration-tests", + "packages/*", ] resolver = "2" @@ -13,8 +13,12 @@ rust-version = "1.78.0" # be sure there is an optimizer release supporting this edition = "2021" [workspace.dependencies] -alloy-primitives = { version = "0.7.6", default-features = false, features = ["std"] } -alloy-sol-types = { version = "0.7.6", default-features = false, features = ["std"] } +alloy-primitives = { version = "0.7.6", default-features = false, features = [ + "std", +] } +alloy-sol-types = { version = "0.7.6", default-features = false, features = [ + "std", +] } anyhow = "1.0.89" assert_ok = "1.0" axelar-wasm-std = { version = "^1.0.0", path = "packages/axelar-wasm-std" } @@ -31,7 +35,9 @@ cw-storage-plus = { version = "1.2.0", features = ["iterator", "macro"] } cw2 = "1.1.0" ed25519-dalek = { version = "2.1.1", default-features = false } error-stack = { version = "0.4.0", features = ["eyre"] } -ethers-contract = { version = "2.0.14", default-features = false, features = ["abigen"] } +ethers-contract = { version = "2.0.14", default-features = false, features = [ + "abigen", +] } ethers-core = "2.0.14" events = { version = "^1.0.0", path = "packages/events" } events-derive = { version = "^1.0.0", path = "packages/events-derive" } @@ -69,6 +75,10 @@ stellar-xdr = { version = "21.2.0" } strum = { version = "0.25", default-features = false, features = ["derive"] } sui-gateway = { version = "^1.0.0", path = "packages/sui-gateway" } sui-types = { version = "^1.0.0", path = "packages/sui-types" } +starknet-types-core = { version = "0.1.7" } +starknet-types = { version = "^1.0.0", path = "packages/starknet-types" } +starknet-core = "0.12.0" +starknet-providers = "0.12.0" syn = "2.0.68" thiserror = "1.0.61" tofn = { version = "1.1" } @@ -77,6 +87,11 @@ tokio-stream = "0.1.11" tokio-util = "0.7.11" voting-verifier = { version = "^1.1.0", path = "contracts/voting-verifier" } axelar-core-std = { version = "^1.0.0", path = "packages/axelar-core-std" } +# Async +futures-concurrency = "7.4" +futures-util = "0.3" +futures = "0.3" +async-trait = "0" [workspace.lints.clippy] arithmetic_side_effects = "deny" diff --git a/contracts/voting-verifier/Cargo.toml b/contracts/voting-verifier/Cargo.toml index 46a00bae1..94796fab8 100644 --- a/contracts/voting-verifier/Cargo.toml +++ b/contracts/voting-verifier/Cargo.toml @@ -5,10 +5,7 @@ rust-version = { workspace = true } edition = { workspace = true } description = "Voting verifier contract" -exclude = [ - "contract.wasm", - "hash.txt" -] +exclude = ["contract.wasm", "hash.txt"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -60,6 +57,7 @@ integration-tests = { workspace = true } multisig = { workspace = true, features = ["test", "library"] } rand = { workspace = true } sha3 = { workspace = true } +starknet-core = { workspace = true } [lints] workspace = true diff --git a/contracts/voting-verifier/src/contract.rs b/contracts/voting-verifier/src/contract.rs index 2aac7083f..7ee7bae88 100644 --- a/contracts/voting-verifier/src/contract.rs +++ b/contracts/voting-verifier/src/contract.rs @@ -123,8 +123,8 @@ mod test { use assert_ok::assert_ok; use axelar_wasm_std::address::AddressFormat; use axelar_wasm_std::msg_id::{ - Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, HexTxHash, - HexTxHashAndEventIndex, MessageIdFormat, + Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, + FieldElementAndEventIndex, HexTxHash, HexTxHashAndEventIndex, MessageIdFormat, }; use axelar_wasm_std::voting::Vote; use axelar_wasm_std::{ @@ -143,6 +143,7 @@ mod test { AuthorizationState, BondingState, Verifier, WeightedVerifier, VERIFIER_WEIGHT, }; use sha3::{Digest, Keccak256, Keccak512}; + use starknet_core::types::Felt; use super::*; use crate::error::ContractError; @@ -237,6 +238,17 @@ mod test { fn message_id(id: &str, index: u64, msg_id_format: &MessageIdFormat) -> nonempty::String { match msg_id_format { + MessageIdFormat::FieldElementAndEventIndex => { + let mut id_bytes: [u8; 32] = Keccak256::digest(id.as_bytes()).into(); + id_bytes[0] = 0; // felt is ~31 bytes + FieldElementAndEventIndex { + tx_hash: Felt::from_bytes_be(&id_bytes), + event_index: index, + } + .to_string() + .parse() + .unwrap() + } MessageIdFormat::HexTxHashAndEventIndex => HexTxHashAndEventIndex { tx_hash: Keccak256::digest(id.as_bytes()).into(), event_index: index, @@ -350,10 +362,53 @@ mod test { address_format: AddressFormat::Eip55, should_fail: true, }, + TestCase { + source_gateway_address: + // 63 chars + "0x06cdc5221388566e09e1a9be3dcfd4b1bbb4abf98296bb4674401a79373cce5" + .to_string() + .to_lowercase(), + address_format: AddressFormat::Starknet, + should_fail: true, + }, + TestCase { + source_gateway_address: + // 62 chars + "0x6cdc5221388566e09e1a9be3dcfd4b1bbb4abf98296bb4674401a79373cce5" + .to_string() + .to_lowercase(), + address_format: AddressFormat::Starknet, + should_fail: true, + }, + TestCase { + source_gateway_address: + // 64 chars, but out of prime field range + "0xff6cdc5221388566e09e1a9be3dcfd4b1bbb4abf98296bb4674401a79373cce5" + .to_string() + .to_lowercase(), + address_format: AddressFormat::Starknet, + should_fail: true, + }, + TestCase { + source_gateway_address: + "0x006cdc5221388566e09e1a9be3dcfd4b1bbb4abf98296bb4674401a79373cce5" + .to_string() + .to_lowercase(), + address_format: AddressFormat::Starknet, + should_fail: false, + }, TestCase { source_gateway_address: "0x4F4495243837681061C4743b74B3eEdf548D56A5" .to_string() .to_lowercase(), + address_format: AddressFormat::Starknet, + should_fail: true, + }, + TestCase { + source_gateway_address: + "0xdb1473ed56ddede13225b99d779ebf9d9011874e26acbb8bfec8b6a43d0fbcaa" + .to_string() + .to_uppercase(), address_format: AddressFormat::Sui, should_fail: true, }, @@ -469,6 +524,22 @@ mod test { ); } + #[test] + fn should_fail_if_messages_have_hex_msg_id_but_contract_expects_field_element() { + let msg_id_format = MessageIdFormat::FieldElementAndEventIndex; + let verifiers = verifiers(2); + let mut deps = setup(verifiers.clone(), &msg_id_format); + + let messages = messages(1, &MessageIdFormat::HexTxHashAndEventIndex); + let msg = ExecuteMsg::VerifyMessages(messages.clone()); + + let err = execute(deps.as_mut(), mock_env(), mock_info(SENDER, &[]), msg).unwrap_err(); + assert_contract_err_strings_equal( + err, + ContractError::InvalidMessageID(messages[0].cc_id.message_id.to_string()), + ); + } + #[test] fn should_fail_if_messages_have_hex_msg_id_but_contract_expects_base58() { let msg_id_format = MessageIdFormat::Base58TxDigestAndEventIndex; @@ -838,6 +909,7 @@ mod test { [ (v, s, MessageIdFormat::HexTxHashAndEventIndex), (v, s, MessageIdFormat::Base58TxDigestAndEventIndex), + (v, s, MessageIdFormat::FieldElementAndEventIndex), ] }) .collect::>(); diff --git a/contracts/voting-verifier/src/events.rs b/contracts/voting-verifier/src/events.rs index 26e7c864e..a617b1c93 100644 --- a/contracts/voting-verifier/src/events.rs +++ b/contracts/voting-verifier/src/events.rs @@ -2,8 +2,8 @@ use std::str::FromStr; use std::vec::Vec; use axelar_wasm_std::msg_id::{ - Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, Bech32mFormat, HexTxHash, - HexTxHashAndEventIndex, MessageIdFormat, + Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, Bech32mFormat, + FieldElementAndEventIndex, HexTxHash, HexTxHashAndEventIndex, MessageIdFormat, }; use axelar_wasm_std::voting::{PollId, Vote}; use axelar_wasm_std::{nonempty, VerificationStatus}; @@ -160,6 +160,16 @@ fn parse_message_id( .map_err(|_| ContractError::InvalidMessageID(message_id.to_string()))?, )) } + MessageIdFormat::FieldElementAndEventIndex => { + let id = FieldElementAndEventIndex::from_str(message_id) + .map_err(|_| ContractError::InvalidMessageID(message_id.to_string()))?; + + Ok(( + id.tx_hash_as_hex(), + u32::try_from(id.event_index) + .map_err(|_| ContractError::InvalidMessageID(message_id.to_string()))?, + )) + } MessageIdFormat::HexTxHashAndEventIndex => { let id = HexTxHashAndEventIndex::from_str(message_id) .map_err(|_| ContractError::InvalidMessageID(message_id.to_string()))?; diff --git a/packages/axelar-wasm-std/Cargo.toml b/packages/axelar-wasm-std/Cargo.toml index a0dee342d..39763faf5 100644 --- a/packages/axelar-wasm-std/Cargo.toml +++ b/packages/axelar-wasm-std/Cargo.toml @@ -5,10 +5,7 @@ rust-version = { workspace = true } edition = { workspace = true } description = "Axelar cosmwasm standard library crate" -exclude = [ - "contract.wasm", - "hash.txt" -] +exclude = ["contract.wasm", "hash.txt"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] @@ -41,7 +38,10 @@ into-inner-derive = { workspace = true } itertools = { workspace = true } lazy_static = "1.4.0" num-traits = { workspace = true } -regex = { version = "1.10.0", default-features = false, features = ["perf", "std"] } +regex = { version = "1.10.0", default-features = false, features = [ + "perf", + "std", +] } report = { workspace = true } schemars = "0.8.10" semver = { workspace = true } @@ -50,17 +50,19 @@ serde_json = "1.0.89" serde_with = { version = "3.11.0", features = ["macros"] } sha3 = { workspace = true } stellar-xdr = { workspace = true } +starknet-types-core = { workspace = true } strum = { workspace = true } sui-types = { workspace = true } thiserror = { workspace = true } valuable = { version = "0.1.0", features = ["derive"] } +crypto-bigint = { version = "0.5.5", features = ["rand_core"] } [dev-dependencies] assert_ok = { workspace = true } cw-multi-test = "0.15.1" goldie = { workspace = true } -hex = { version = "0.4.3", default-features = false } rand = { workspace = true } +hex = { version = "0.4.3", default-features = false } [lints] workspace = true diff --git a/packages/axelar-wasm-std/src/address.rs b/packages/axelar-wasm-std/src/address.rs index ea1544c44..76e85fb38 100644 --- a/packages/axelar-wasm-std/src/address.rs +++ b/packages/axelar-wasm-std/src/address.rs @@ -4,9 +4,12 @@ use alloy_primitives::Address; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Api}; use error_stack::{bail, Result, ResultExt}; +use starknet_types_core::felt::Felt; use stellar_xdr::curr::ScAddress; use sui_types::SuiAddress; +use crate::utils::does_felt_overflow_from_str; + #[derive(thiserror::Error)] #[cw_serde] pub enum Error { @@ -19,6 +22,7 @@ pub enum AddressFormat { Eip55, Sui, Stellar, + Starknet, } pub fn validate_address(address: &str, format: &AddressFormat) -> Result<(), Error> { @@ -38,6 +42,46 @@ pub fn validate_address(address: &str, format: &AddressFormat) -> Result<(), Err ScAddress::from_str(address) .change_context(Error::InvalidAddress(address.to_string()))?; } + AddressFormat::Starknet => { + // Contract addresses in Starknet are Felts, which are decimals in a + // prime field, which fit in 252 bytes and can't exceed that prime field. + // We'll only accept hex representation of the Felts, because they're the most + // commonly used representation for addresses. + // + // We'll only accept 64 char hex strings. + // 62 and 63 hex string chars is also a valid address but we expect those to be padded + // with zeroes. + + if !address.starts_with("0x") { + bail!(Error::InvalidAddress("0x prefix is missing".to_string())) + } + + let trimmed_addr = address.trim_start_matches("0x"); + if trimmed_addr.len() != 64 { + bail!(Error::InvalidAddress(format!( + "hex string is not 64 chars: {}", + address.to_string() + ))) + } + + let valid_hex_felt = Felt::from_hex(address); + if valid_hex_felt.is_err() { + bail!(Error::InvalidAddress(format!( + "not a valid hex field element: {}", + address.to_string() + ))) + } + + if does_felt_overflow_from_str(trimmed_addr) { + bail!(Error::InvalidAddress( + format!( + "field element overflows MAX value of 2^251 + 17 * 2^192: {}", + address + ) + .to_string() + )) + } + } } Ok(()) @@ -187,4 +231,96 @@ mod tests { address::Error::InvalidAddress(..) ); } + + #[test] + fn validate_starknet_address() { + // 0 prefixed field element + // 64 chars + let addr = "0x0282b4492e08d8b6bbec8dfe7412e42e897eef9c080c5b97be1537433e583bdc"; + assert_ok!(address::validate_address( + addr, + &address::AddressFormat::Starknet + )); + + // 0x prefix removed from string, but padded with 0 + // 64 chars + let zero_x_removed = "0282b4492e08d8b6bbec8dfe7412e42e897eef9c080c5b97be1537433e583bdc"; + assert_err_contains!( + address::validate_address(zero_x_removed, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // 0x0 prefix removed from string. + // Commonly a `0` is prefixed to the field element, in order to make it a valid hex. + // Originally the felt is 63 chars, which is an invalid hex by itself + // 63 chars + let zero_x_zero_removed = "282b4492e08d8b6bbec8dfe7412e42e897eef9c080c5b97be1537433e583bdc"; + assert_err_contains!( + address::validate_address(zero_x_zero_removed, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // 0 prefix removed from string, but 0x is left in. + // This is an invalid 63char hex by itself, but a valid field element. + // 63 chars. + let zero_removed = "0x282b4492e08d8b6bbec8dfe7412e42e897eef9c080c5b97be1537433e583bdc"; + assert_err_contains!( + address::validate_address(zero_removed, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // invalid hex (starts with `q`) + let invalid_hex = "0xq282b4492e08d8b6bbec8dfe7412e42e897eef9c080c5b97be1537433e583bdc"; + assert_err_contains!( + address::validate_address(invalid_hex, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // more than 64 chars is invalid + let more_than_64 = "0x282b4492e08d8b6bbec8dfe7412e42e897eef9c080c5b97be1537433e583bdc123"; + assert_err_contains!( + address::validate_address(more_than_64, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // less than 63 chars is invalid + let less_than_63 = "0x123"; + assert_err_contains!( + address::validate_address(less_than_63, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + let overflown_felt_with_one = + "0x080000006b9f1bed878fcc665f2ca1a6afd545a6b864d8400000000000000001"; + assert_err_contains!( + address::validate_address(overflown_felt_with_one, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // overflowed field element (added a 64th char, other than 0) + let overflown_felt = "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; + assert_err_contains!( + address::validate_address(overflown_felt, &address::AddressFormat::Starknet), + address::Error, + address::Error::InvalidAddress(..) + ); + + // uppercase string field element + let upper_case_invalid = addr.to_uppercase(); + assert_err_contains!( + address::validate_address( + upper_case_invalid.as_str(), + &address::AddressFormat::Starknet + ), + address::Error, + address::Error::InvalidAddress(..) + ); + } } diff --git a/packages/axelar-wasm-std/src/msg_id/mod.rs b/packages/axelar-wasm-std/src/msg_id/mod.rs index c4cc1b972..1107b98c2 100644 --- a/packages/axelar-wasm-std/src/msg_id/mod.rs +++ b/packages/axelar-wasm-std/src/msg_id/mod.rs @@ -7,6 +7,7 @@ use error_stack::Report; pub use self::base_58_event_index::Base58TxDigestAndEventIndex; pub use self::base_58_solana_event_index::Base58SolanaTxSignatureAndEventIndex; pub use self::bech32m::Bech32mFormat; +pub use self::starknet_field_element_event_index::FieldElementAndEventIndex; pub use self::tx_hash::HexTxHash; pub use self::tx_hash_event_index::HexTxHashAndEventIndex; use crate::nonempty; @@ -14,6 +15,7 @@ use crate::nonempty; mod base_58_event_index; mod base_58_solana_event_index; mod bech32m; +mod starknet_field_element_event_index; mod tx_hash; mod tx_hash_event_index; @@ -32,6 +34,8 @@ pub enum Error { InvalidBech32mFormat(String), #[error("Invalid bech32m: '{0}'")] InvalidBech32m(String), + #[error("invalid field element '{0}'")] + InvalidFieldElement(String), } /// Any message id format must implement this trait. @@ -48,6 +52,7 @@ pub trait MessageId: FromStr + Display {} /// enum to pass to the router when registering a new chain #[cw_serde] pub enum MessageIdFormat { + FieldElementAndEventIndex, HexTxHashAndEventIndex, Base58TxDigestAndEventIndex, Base58SolanaTxSignatureAndEventIndex, @@ -61,6 +66,9 @@ pub enum MessageIdFormat { // function the router calls to verify msg ids pub fn verify_msg_id(message_id: &str, format: &MessageIdFormat) -> Result<(), Report> { match format { + MessageIdFormat::FieldElementAndEventIndex => { + FieldElementAndEventIndex::from_str(message_id).map(|_| ()) + } MessageIdFormat::HexTxHashAndEventIndex => { HexTxHashAndEventIndex::from_str(message_id).map(|_| ()) } diff --git a/packages/axelar-wasm-std/src/msg_id/starknet_field_element_event_index.rs b/packages/axelar-wasm-std/src/msg_id/starknet_field_element_event_index.rs new file mode 100644 index 000000000..4ac459148 --- /dev/null +++ b/packages/axelar-wasm-std/src/msg_id/starknet_field_element_event_index.rs @@ -0,0 +1,284 @@ +use core::fmt; +use std::fmt::Display; +use std::str::FromStr; + +use cosmwasm_std::HexBinary; +use error_stack::Report; +use lazy_static::lazy_static; +use regex::Regex; +use serde_with::DeserializeFromStr; +use starknet_types_core::felt::Felt; + +use super::Error; +use crate::nonempty; +use crate::utils::does_felt_overflow_from_str; + +#[derive(Debug, DeserializeFromStr, Clone)] +pub struct FieldElementAndEventIndex { + pub tx_hash: Felt, + pub event_index: u64, +} + +impl FieldElementAndEventIndex { + pub fn tx_hash_as_hex(&self) -> nonempty::String { + format!("0x{}", self.tx_hash_as_hex_no_prefix()) + .try_into() + .expect("failed to convert tx hash to non-empty string") + } + + pub fn tx_hash_as_hex_no_prefix(&self) -> nonempty::String { + HexBinary::from(self.tx_hash.to_bytes_be()) + .to_hex() + .to_string() + .try_into() + .expect("failed to convert tx hash to non-empty string") + } + + pub fn new + FromStr>( + tx_id: T, + event_index: impl Into, + ) -> Result { + Ok(Self { + tx_hash: tx_id.into(), + event_index: event_index.into(), + }) + } +} + +// A valid field element is max 252 bits, meaning max 63 hex characters after 0x. +// We require the hex to be 64 characters, meaning that it should be padded with zeroes in order +// for us to consider it valid. +const PATTERN: &str = "^(0x[0-9a-f]{64})-(0|[1-9][0-9]*)$"; +lazy_static! { + static ref REGEX: Regex = Regex::new(PATTERN).expect("invalid regex"); +} + +impl FromStr for FieldElementAndEventIndex { + type Err = Report; + + fn from_str(message_id: &str) -> Result + where + Self: Sized, + { + // the PATTERN has exactly two capture groups, so the groups can be extracted safely + let (_, [tx_id, event_index]) = REGEX + .captures(message_id) + .ok_or(Error::InvalidMessageID { + id: message_id.to_string(), + expected_format: PATTERN.to_string(), + })? + .extract(); + let tx_id_chunk = &tx_id[2..]; + let felt = Felt::from_hex(tx_id_chunk) + .map_err(|_| Error::InvalidFieldElement(tx_id_chunk.to_string()))?; + + if does_felt_overflow_from_str(tx_id_chunk) { + Err(Error::InvalidFieldElement( + format!( + "field element overflows MAX value of 2^251 + 17 * 2^192: {}", + tx_id_chunk + ) + .to_string(), + ))? + } + Ok(FieldElementAndEventIndex { + tx_hash: felt, + event_index: event_index + .parse() + .map_err(|_| Error::EventIndexOverflow(message_id.to_string()))?, + }) + } +} + +// pad the FieldElement with zeroes +impl Display for FieldElementAndEventIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "0x{:064x}-{}", self.tx_hash, self.event_index) + } +} + +impl From for nonempty::String { + fn from(msg_id: FieldElementAndEventIndex) -> Self { + msg_id + .to_string() + .try_into() + .expect("failed to convert msg id to non-empty string") + } +} + +#[cfg(test)] +mod tests { + + use crypto_bigint::rand_core::OsRng; + use crypto_bigint::{NonZero, RandomMod, U256}; + + use super::*; + + fn random_hash() -> String { + let field_element_max = "080000006B9F1BED878FCC665F2CA1A6AFD545A6B864D8400000000000000000"; + let modulus = NonZero::new(U256::from_be_hex(field_element_max)).unwrap(); + let n = U256::random_mod(&mut OsRng, &modulus); + + format!("0x{:064x}", n) + } + + fn random_event_index() -> u64 { + rand::random() + } + + #[test] + fn should_parse_msg_id() { + let res = FieldElementAndEventIndex::from_str( + "0x0670d1dd42a19cb229bb4378b58b9c3e76aa43edaaea46845cd8c456c1224d89-0", + ); + assert!(res.is_ok()); + + for _ in 0..1000 { + let tx_hash = random_hash(); + let event_index = random_event_index(); + let msg_id = format!("{}-{}", tx_hash, event_index); + + let res = FieldElementAndEventIndex::from_str(&msg_id); + let parsed = res.unwrap(); + assert_eq!(parsed.event_index, event_index); + assert_eq!(parsed.tx_hash_as_hex(), tx_hash.try_into().unwrap(),); + assert_eq!(parsed.to_string(), msg_id); + } + } + + #[test] + fn should_not_parse_msg_id_overflowing_felt() { + let res = FieldElementAndEventIndex::from_str( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff-0", + ); + assert!(res.is_err()); + + // Felt::MAX + 1 + let res = FieldElementAndEventIndex::from_str( + "0x080000006b9f1bed878fcc665f2ca1a6afd545a6b864d8400000000000000001-0", + ); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_63_or_62_chars() { + let res = FieldElementAndEventIndex::from_str( + "0x670d1dd42a19cb229bb4378b58b9c3e76aa43edaaea46845cd8c456c1224d89-0", + ); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str( + "0x17f60b1e54f3b012bffc2b328070fde2b5dae12220c985f098fb8e36338472-0", + ); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_wrong_length_tx_hash() { + let tx_hash = random_hash(); + // too long + let res = FieldElementAndEventIndex::from_str(&format!("{}ff-{}", tx_hash, 1)); + assert!(res.is_err()); + + // too short + let res = FieldElementAndEventIndex::from_str(&format!( + "{}-{}", + &tx_hash[..tx_hash.len() - 2], + 1 + )); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_uppercase_tx_hash() { + let tx_hash = &random_hash()[2..]; + let res = + FieldElementAndEventIndex::from_str(&format!("0x{}-{}", tx_hash.to_uppercase(), 1)); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_non_hex_tx_hash() { + let msg_id = "82GKYvWv5EKm7jnYksHoh3u5M2RxHN2boPreM8Df4ej9-1"; + let res = FieldElementAndEventIndex::from_str(msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_without_0x() { + let msg_id = "7cedbb3799cd99636045c84c5c55aef8a138f107ac8ba53a08cad1070ba4385b-1"; + let res = FieldElementAndEventIndex::from_str(msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_missing_event_index() { + let msg_id = random_hash(); + let res = FieldElementAndEventIndex::from_str(&msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_wrong_separator() { + let tx_hash = random_hash(); + let event_index = random_event_index(); + + let res = FieldElementAndEventIndex::from_str(&format!("{}:{}", tx_hash, event_index)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}_{}", tx_hash, event_index)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}+{}", tx_hash, event_index)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}{}", tx_hash, event_index)); + assert!(res.is_err()); + + for _ in 0..10 { + let random_sep: char = rand::random(); + if random_sep == '-' { + continue; + } + let res = FieldElementAndEventIndex::from_str(&format!( + "{}{}{}", + tx_hash, random_sep, event_index + )); + assert!(res.is_err()); + } + } + + #[test] + fn should_not_parse_msg_id_with_event_index_with_leading_zeroes() { + let tx_hash = random_hash(); + let res = FieldElementAndEventIndex::from_str(&format!("{}-01", tx_hash)); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_non_integer_event_index() { + let tx_hash = random_hash(); + let res = FieldElementAndEventIndex::from_str(&format!("{}-1.0", tx_hash)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}-0x00", tx_hash)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}-foobar", tx_hash)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}-true", tx_hash)); + assert!(res.is_err()); + + let res = FieldElementAndEventIndex::from_str(&format!("{}-", tx_hash)); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_overflowing_event_index() { + let event_index: u64 = u64::MAX; + let tx_hash = random_hash(); + let res = FieldElementAndEventIndex::from_str(&format!("{}-{}1", tx_hash, event_index)); + assert!(res.is_err()); + } +} diff --git a/packages/axelar-wasm-std/src/utils.rs b/packages/axelar-wasm-std/src/utils.rs index e45c0695a..292deb920 100644 --- a/packages/axelar-wasm-std/src/utils.rs +++ b/packages/axelar-wasm-std/src/utils.rs @@ -1,3 +1,6 @@ +use crypto_bigint::U256; +use starknet_types_core::felt::Felt; + pub trait TryMapExt { type Monad; fn try_map(self, func: impl FnMut(T) -> Result) -> Result, E>; @@ -19,6 +22,22 @@ impl TryMapExt for Vec { } } +/// since the `Felt` type doesn't error on overflow, we have to implement that check +pub fn does_felt_overflow_from_slice(felt_hex_slice: &[u8]) -> bool { + if felt_hex_slice.len() > 32 { + return true; + } + let felt_max_hex_str = format!("{:064x}", Felt::MAX); + U256::from_be_slice(felt_hex_slice) > U256::from_be_hex(&felt_max_hex_str) +} + +/// since the `Felt` type doesn't error on overflow, we have to implement that check +pub fn does_felt_overflow_from_str(felt_hex_str: &str) -> bool { + let felt_hex_str = felt_hex_str.trim_start_matches("0x"); + let felt_max_hex_str = format!("{:064x}", Felt::MAX); + U256::from_be_hex(felt_hex_str) > U256::from_be_hex(&felt_max_hex_str) +} + #[cfg(test)] mod test { use super::*;