From 155034a70e9ef510905a6e3852d9dbe1d82c8a1e Mon Sep 17 00:00:00 2001 From: k4m4 Date: Mon, 15 Jul 2024 13:29:59 +0300 Subject: [PATCH] feat: add support for XRPL message ID --- contracts/voting-verifier/src/contract.rs | 8 +- contracts/voting-verifier/src/events.rs | 8 +- packages/axelar-wasm-std/src/msg_id/mod.rs | 26 ++++ .../axelar-wasm-std/src/msg_id/tx_hash.rs | 141 ++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 packages/axelar-wasm-std/src/msg_id/tx_hash.rs diff --git a/contracts/voting-verifier/src/contract.rs b/contracts/voting-verifier/src/contract.rs index 921a21bbd..a4b9d2bee 100644 --- a/contracts/voting-verifier/src/contract.rs +++ b/contracts/voting-verifier/src/contract.rs @@ -99,7 +99,7 @@ pub fn migrate( mod test { use axelar_wasm_std::msg_id::{ Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, HexTxHashAndEventIndex, - MessageIdFormat, + HexTxHash, MessageIdFormat, }; use axelar_wasm_std::voting::Vote; use axelar_wasm_std::{nonempty, MajorityThreshold, Threshold, VerificationStatus}; @@ -211,6 +211,12 @@ mod test { .to_string() .parse() .unwrap(), + MessageIdFormat::HexTxHash => HexTxHash { + tx_hash: Keccak256::digest(id.as_bytes()).into(), + } + .to_string() + .parse() + .unwrap(), MessageIdFormat::Base58TxDigestAndEventIndex => Base58TxDigestAndEventIndex { tx_digest: Keccak256::digest(id.as_bytes()).into(), event_index: index, diff --git a/contracts/voting-verifier/src/events.rs b/contracts/voting-verifier/src/events.rs index 764a62103..6ea6fc0a7 100644 --- a/contracts/voting-verifier/src/events.rs +++ b/contracts/voting-verifier/src/events.rs @@ -3,7 +3,7 @@ use std::vec::Vec; use axelar_wasm_std::msg_id::{ Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, HexTxHashAndEventIndex, - MessageIdFormat, + HexTxHash, MessageIdFormat, }; use axelar_wasm_std::voting::{PollId, Vote}; use axelar_wasm_std::{nonempty, VerificationStatus}; @@ -137,6 +137,12 @@ fn parse_message_id( Ok((id.tx_hash_as_hex(), id.event_index)) } + MessageIdFormat::HexTxHash => { + let id = HexTxHash::from_str(&message_id) + .map_err(|_| ContractError::InvalidMessageID(message_id.into()))?; + + Ok((id.tx_hash_as_hex(), 0)) + } MessageIdFormat::Base58SolanaTxSignatureAndEventIndex => { let id = Base58SolanaTxSignatureAndEventIndex::from_str(&message_id) .map_err(|_| ContractError::InvalidMessageID(message_id.into()))?; diff --git a/packages/axelar-wasm-std/src/msg_id/mod.rs b/packages/axelar-wasm-std/src/msg_id/mod.rs index 26c33de5b..a2a69ff23 100644 --- a/packages/axelar-wasm-std/src/msg_id/mod.rs +++ b/packages/axelar-wasm-std/src/msg_id/mod.rs @@ -7,10 +7,12 @@ use error_stack::Report; pub use self::base_58_event_index::Base58TxDigestAndEventIndex; pub use self::base_58_solana_event_index::Base58SolanaTxSignatureAndEventIndex; pub use self::tx_hash_event_index::HexTxHashAndEventIndex; +pub use self::tx_hash::HexTxHash; mod base_58_event_index; mod base_58_solana_event_index; mod tx_hash_event_index; +mod tx_hash; #[derive(thiserror::Error)] #[cw_serde] @@ -40,6 +42,7 @@ pub trait MessageId: FromStr + Display {} #[cw_serde] pub enum MessageIdFormat { HexTxHashAndEventIndex, + HexTxHash, Base58TxDigestAndEventIndex, Base58SolanaTxSignatureAndEventIndex, } @@ -50,6 +53,9 @@ pub fn verify_msg_id(message_id: &str, format: &MessageIdFormat) -> Result<(), R MessageIdFormat::HexTxHashAndEventIndex => { HexTxHashAndEventIndex::from_str(message_id).map(|_| ()) } + MessageIdFormat::HexTxHash => { + HexTxHash::from_str(message_id).map(|_| ()) + } MessageIdFormat::Base58TxDigestAndEventIndex => { Base58TxDigestAndEventIndex::from_str(message_id).map(|_| ()) } @@ -62,6 +68,7 @@ pub fn verify_msg_id(message_id: &str, format: &MessageIdFormat) -> Result<(), R #[cfg(test)] mod test { use super::tx_hash_event_index::HexTxHashAndEventIndex; + use super::tx_hash::HexTxHash; use crate::msg_id::base_58_event_index::Base58TxDigestAndEventIndex; use crate::msg_id::{verify_msg_id, MessageIdFormat}; @@ -75,6 +82,15 @@ mod test { assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHashAndEventIndex).is_ok()); } + #[test] + fn should_verify_hex_tx_hash_msg_id() { + let msg_id = HexTxHash { + tx_hash: [1; 32], + } + .to_string(); + assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHash).is_ok()); + } + #[test] fn should_verify_base_58_tx_digest_event_index_msg_id() { let msg_id = Base58TxDigestAndEventIndex { @@ -89,6 +105,7 @@ mod test { fn should_not_verify_invalid_msg_id() { let msg_id = "foobar"; assert!(verify_msg_id(msg_id, &MessageIdFormat::HexTxHashAndEventIndex).is_err()); + assert!(verify_msg_id(msg_id, &MessageIdFormat::HexTxHash).is_err()); } #[test] @@ -99,12 +116,21 @@ mod test { } .to_string(); assert!(verify_msg_id(&msg_id, &MessageIdFormat::Base58TxDigestAndEventIndex).is_err()); + assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHash).is_err()); let msg_id = Base58TxDigestAndEventIndex { tx_digest: [1; 32], event_index: 0, } .to_string(); + assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHash).is_err()); assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHashAndEventIndex).is_err()); + + let msg_id = HexTxHash { + tx_hash: [1; 32], + } + .to_string(); + assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHashAndEventIndex).is_err()); + assert!(verify_msg_id(&msg_id, &MessageIdFormat::Base58TxDigestAndEventIndex).is_err()); } } diff --git a/packages/axelar-wasm-std/src/msg_id/tx_hash.rs b/packages/axelar-wasm-std/src/msg_id/tx_hash.rs new file mode 100644 index 000000000..17d63617f --- /dev/null +++ b/packages/axelar-wasm-std/src/msg_id/tx_hash.rs @@ -0,0 +1,141 @@ +use core::fmt; +use std::{fmt::Display, str::FromStr}; + +use cosmwasm_std::HexBinary; +use error_stack::{Report, ResultExt}; +use lazy_static::lazy_static; +use regex::Regex; + +use super::Error; +use crate::{hash::Hash, nonempty}; + +pub struct HexTxHash { + pub tx_hash: Hash, +} + +impl HexTxHash { + pub fn tx_hash_as_hex(&self) -> nonempty::String { + HexBinary::from(self.tx_hash).to_hex() + .try_into() + .expect("failed to convert tx hash to non-empty string") + } + + pub fn new(tx_id: impl Into<[u8; 32]>) -> Self { + Self { + tx_hash: tx_id.into(), + } + } +} + +const PATTERN: &str = "^([0-9a-f]{64})$"; +lazy_static! { + static ref REGEX: Regex = Regex::new(PATTERN).expect("invalid regex"); +} + +impl FromStr for HexTxHash { + type Err = Report; + + fn from_str(message_id: &str) -> Result + where + Self: Sized, + { + // the PATTERN has exactly one capture group, so the group can be extracted safely + let (_, [tx_id]) = REGEX + .captures(message_id) + .ok_or(Error::InvalidMessageID { + id: message_id.to_string(), + expected_format: PATTERN.to_string(), + })? + .extract(); + Ok(HexTxHash { + tx_hash: HexBinary::from_hex(&tx_id) + .change_context(Error::InvalidTxHash(message_id.to_string()))? + .as_slice() + .try_into() + .map_err(|_| Error::InvalidTxHash(message_id.to_string()))?, + }) + } +} + +impl Display for HexTxHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + HexBinary::from(self.tx_hash).to_hex(), + ) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + fn random_hash() -> String { + let mut bytes = vec![]; + for _ in 0..32 { + let byte: u8 = rand::random(); + bytes.push(byte) + } + HexBinary::from(bytes).to_hex() + } + + #[test] + fn should_parse_msg_id() { + let res = HexTxHash::from_str( + "7cedbb3799cd99636045c84c5c55aef8a138f107ac8ba53a08cad1070ba4385b", + ); + assert!(res.is_ok()); + + for _ in 0..1000 { + let msg_id = random_hash(); + + let res = HexTxHash::from_str(&msg_id); + let parsed = res.unwrap(); + assert_eq!(parsed.tx_hash_as_hex(), msg_id.clone().try_into().unwrap()); + assert_eq!(parsed.to_string(), msg_id); + } + } + + #[test] + fn should_not_parse_msg_id_with_wrong_length_tx_hash() { + let tx_hash = random_hash(); + // too long + let res = HexTxHash::from_str(&format!("{}ff", tx_hash)); + assert!(res.is_err()); + + // too short + let res = + HexTxHash::from_str(&format!("{}", &tx_hash[..tx_hash.len() - 2])); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_uppercase_tx_hash() { + let tx_hash = &random_hash(); + let res = HexTxHash::from_str(&tx_hash.to_uppercase()); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_non_hex_tx_hash() { + let msg_id = "82GKYvWv5EKm7jnYksHoh3u5M2RxHN2boPreM8Df4ej9"; + let res = HexTxHash::from_str(msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_0x() { + let msg_id = "0x7cedbb3799cd99636045c84c5c55aef8a138f107ac8ba53a08cad1070ba4385b-1"; + let res = HexTxHash::from_str(msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_event_index() { + let tx_hash = random_hash(); + let res = HexTxHash::from_str(&format!("{}-1", tx_hash)); + assert!(res.is_err()); + } +}