From addfec503899b83146a0dee8301204e00f1fb9ce Mon Sep 17 00:00:00 2001 From: eloylp Date: Mon, 8 Jul 2024 15:41:56 +0200 Subject: [PATCH] feat: add support for solana message id's --- contracts/voting-verifier/src/contract.rs | 10 + contracts/voting-verifier/src/events.rs | 7 + packages/axelar-wasm-std/src/hash.rs | 2 +- .../src/msg_id/base_58_solana_event_index.rs | 362 ++++++++++++++++++ packages/axelar-wasm-std/src/msg_id/mod.rs | 9 +- 5 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 packages/axelar-wasm-std/src/msg_id/base_58_solana_event_index.rs diff --git a/contracts/voting-verifier/src/contract.rs b/contracts/voting-verifier/src/contract.rs index 52a9795dd..174e7ad3b 100644 --- a/contracts/voting-verifier/src/contract.rs +++ b/contracts/voting-verifier/src/contract.rs @@ -106,6 +106,7 @@ mod test { use axelar_wasm_std::{ msg_id::{ base_58_event_index::Base58TxDigestAndEventIndex, + base_58_solana_event_index::Base58SolanaTxDigestAndEventIndex, tx_hash_event_index::HexTxHashAndEventIndex, MessageIdFormat, }, nonempty, @@ -224,6 +225,15 @@ mod test { .to_string() .parse() .unwrap(), + MessageIdFormat::Base58SolanaTxDigestAndEventIndex => { + Base58SolanaTxDigestAndEventIndex::new_from_b58_encoded_signature_and_index( + id, index, + ) + .unwrap() + .to_string() + .parse() + .unwrap() + } } } diff --git a/contracts/voting-verifier/src/events.rs b/contracts/voting-verifier/src/events.rs index c2d0a9436..e54ab3336 100644 --- a/contracts/voting-verifier/src/events.rs +++ b/contracts/voting-verifier/src/events.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use std::vec::Vec; use axelar_wasm_std::msg_id::base_58_event_index::Base58TxDigestAndEventIndex; +use axelar_wasm_std::msg_id::base_58_solana_event_index::Base58SolanaTxDigestAndEventIndex; use axelar_wasm_std::msg_id::tx_hash_event_index::HexTxHashAndEventIndex; use axelar_wasm_std::msg_id::MessageIdFormat; use cosmwasm_schema::cw_serde; @@ -137,6 +138,12 @@ fn parse_message_id( Ok((id.tx_hash_as_hex(), id.event_index)) } + MessageIdFormat::Base58SolanaTxDigestAndEventIndex => { + let id = Base58SolanaTxDigestAndEventIndex::from_str(&message_id) + .map_err(|_| ContractError::InvalidMessageID(message_id.into()))?; + + Ok((id.tx_digest_as_base58(), id.event_index)) + } } } diff --git a/packages/axelar-wasm-std/src/hash.rs b/packages/axelar-wasm-std/src/hash.rs index 224b450a9..1caf3254a 100644 --- a/packages/axelar-wasm-std/src/hash.rs +++ b/packages/axelar-wasm-std/src/hash.rs @@ -1 +1 @@ -pub type Hash = [u8; 32]; +pub type Hash = [u8; 32]; \ No newline at end of file diff --git a/packages/axelar-wasm-std/src/msg_id/base_58_solana_event_index.rs b/packages/axelar-wasm-std/src/msg_id/base_58_solana_event_index.rs new file mode 100644 index 000000000..3aa49c6c5 --- /dev/null +++ b/packages/axelar-wasm-std/src/msg_id/base_58_solana_event_index.rs @@ -0,0 +1,362 @@ +use core::fmt; +use std::{array::TryFromSliceError, fmt::Display, str::FromStr}; + +use error_stack::{Report, ResultExt}; +use lazy_static::lazy_static; +use regex::Regex; + +use super::Error; +use crate::nonempty; + +type RawSignature = [u8; 64]; + +pub struct Base58SolanaTxDigestAndEventIndex { + // Base64 decoded bytes of the Solana signature. + pub signature: RawSignature, + pub event_index: u32, +} + +impl Base58SolanaTxDigestAndEventIndex { + pub fn tx_digest_as_base58(&self) -> nonempty::String { + bs58::encode(self.signature) + .into_string() + .try_into() + .expect("failed to convert tx hash to non-empty string") + } + + pub fn new(tx_id: impl Into, event_index: impl Into) -> Self { + Self { + signature: tx_id.into(), + event_index: event_index.into(), + } + } + + pub fn new_from_b58_encoded_signature_and_index( + signature: &str, + event_index: impl Into, + ) -> Result> { + Ok(Self { + signature: decode_b58_signature(signature)?, + event_index: event_index.into(), + }) + } +} + +fn decode_b58_signature(signature: &str) -> Result> { + Ok(bs58::decode(signature) + .into_vec() + .change_context(Error::InvalidTxDigest(signature.to_string()))? + .as_slice() + .try_into() + .map_err(|e: TryFromSliceError| { + Error::InvalidTxDigest(format!( + "Signature - {}: {}", + signature, + e + )) + })?) +} + +const PATTERN: &str = "^([1-9A-HJ-NP-Za-km-z]{32,88})-(0|[1-9][0-9]*)$"; +lazy_static! { + static ref REGEX: Regex = Regex::new(PATTERN).expect("invalid regex"); +} + +impl FromStr for Base58SolanaTxDigestAndEventIndex { + 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 (_, [signature, event_index]) = REGEX + .captures(message_id) + .ok_or(Error::InvalidMessageID { + id: message_id.to_string(), + expected_format: PATTERN.to_string(), + })? + .extract(); + + Ok(Base58SolanaTxDigestAndEventIndex { + signature: decode_b58_signature(signature)?, + event_index: event_index + .parse() + .map_err(|_| Error::EventIndexOverflow(message_id.to_string()))?, + }) + } +} + +impl Display for Base58SolanaTxDigestAndEventIndex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}-{}", + bs58::encode(self.signature).into_string(), + self.event_index + ) + } +} + +#[cfg(test)] +mod tests { + + use hex::ToHex; + + use super::*; + + fn random_bytes() -> [u8; 64] { + let mut bytes = [0; 64]; + for b in &mut bytes { + *b = rand::random(); + } + bytes + } + + fn random_tx_digest() -> String { + bs58::encode(random_bytes()).into_string() + } + + fn random_event_index() -> u32 { + rand::random() + } + + #[test] + fn should_parse_msg_id() { + let res = Base58SolanaTxDigestAndEventIndex::from_str( + "4hHzKKdpXH2QMB5Jm11YR48cLqUJb9Cwq2YL3tveVTPeFkZaLP8cdcH5UphVPJ7kYwCUCRLnywd3xkUhb4ZYWtf5-0", + ); + res.unwrap(); + + for _ in 0..1000 { + let tx_digest = random_tx_digest(); + let event_index = random_event_index(); + let msg_id = format!("{}-{}", tx_digest, event_index); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&msg_id); + let parsed = res.unwrap(); + assert_eq!(parsed.event_index, event_index); + assert_eq!(parsed.tx_digest_as_base58(), tx_digest.try_into().unwrap()); + assert_eq!(parsed.to_string(), msg_id); + } + } + + #[test] + fn should_not_parse_msg_id_with_wrong_length_base58_tx_digest() { + let tx_digest = random_tx_digest(); + let event_index = random_event_index(); + + // too long + let msg_id = format!("{}{}-{}", tx_digest, tx_digest, event_index); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&msg_id); + assert!(res.is_err()); + + // too short + let msg_id = format!("{}-{}", &tx_digest[0..30], event_index); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&msg_id); + assert!(res.is_err()); + } + + #[test] + fn leading_ones_should_not_be_ignored() { + let tx_digest = random_tx_digest(); + let event_index = random_event_index(); + + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("1{}-{}", tx_digest, event_index)); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "11{}-{}", + tx_digest, event_index + )); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_correct_length_base58_but_wrong_length_hex() { + // this is 88 chars and valid base58, but will encode to 33 bytes + // the leading 1s are encoded as 00 in hex and thus result in too many bytes + let tx_digest = "1111KKdpXH2QMB5Jm11YR48cLqUJb9Cwq2YL3tveVTPeFkZaLP8cdcH5UphVPJ7kYwCUCRLnywd3xkUhb4ZYWtf5"; + let event_index = random_event_index(); + let msg_id = format!("{}-{}", tx_digest, event_index); + + assert!(REGEX.captures(&msg_id).is_some()); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&msg_id); + assert!(res.is_err()); + + // this is 44 chars and valid base 58, but will encode to 33 bytes + // (z is the largest base58 digit, and so this will overflow 2^256) + let tx_digest = "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"; + assert_eq!(tx_digest.len(), 44); + let msg_id = format!("{}-{}", tx_digest, event_index); + + assert!(REGEX.captures(&msg_id).is_some()); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_parse_msg_id_less_than_88_chars_tx_digest() { + // the tx digest can be less than 88 chars in the presence of leading 1s (00 in hex) + let tx_digest = "1111KKdpXH2QMB5Jm11YR48cLqUJb9Cwq2YL3tveVTPeFkZaLP8cdcH5UphVPJ7kYwCUCRLnywd3xkUhb4ZYW"; + assert!(tx_digest.len() < 88); + let event_index = random_event_index(); + + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-{}", tx_digest, event_index)); + assert!(res.is_ok()); + } + + #[test] + fn should_not_parse_msg_id_with_invalid_base58() { + let tx_digest = random_tx_digest(); + let event_index = random_event_index(); + + // 0, O and I are invalid base58 chars + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "0{}-{}", + &tx_digest[1..], + event_index + )); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "I{}-{}", + &tx_digest[1..], + event_index + )); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "O{}-{}", + &tx_digest[1..], + event_index + )); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_hex_tx_digest() { + let tx_digest = random_tx_digest(); + let event_index = random_event_index(); + let tx_digest_hex = bs58::decode(tx_digest) + .into_vec() + .unwrap() + .encode_hex::(); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "{}-{}", + tx_digest_hex, event_index + )); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "0x{}-{}", + tx_digest_hex, event_index + )); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_missing_event_index() { + let msg_id = random_tx_digest(); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&msg_id); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_wrong_separator() { + let tx_digest = random_tx_digest(); + let event_index = random_event_index(); + + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}:{}", tx_digest, event_index)); + assert!(res.is_err()); + + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}_{}", tx_digest, event_index)); + assert!(res.is_err()); + + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}+{}", tx_digest, event_index)); + assert!(res.is_err()); + + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}{}", tx_digest, event_index)); + assert!(res.is_err()); + + for _ in 0..10 { + let random_sep: char = rand::random(); + if random_sep == '-' { + continue; + } + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!( + "{}{}{}", + tx_digest, random_sep, event_index + )); + assert!(res.is_err()); + } + } + + #[test] + fn should_not_parse_msg_id_with_event_index_with_leading_zeroes() { + let tx_digest = random_tx_digest(); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-01", tx_digest)); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_non_integer_event_index() { + let tx_digest = random_tx_digest(); + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-1.0", tx_digest)); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-0x00", tx_digest)); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-foobar", tx_digest)); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-true", tx_digest)); + assert!(res.is_err()); + + let res = Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-", tx_digest)); + assert!(res.is_err()); + } + + #[test] + fn should_not_parse_msg_id_with_overflowing_event_index() { + let event_index: u64 = u64::MAX; + let tx_digest = random_tx_digest(); + let res = + Base58SolanaTxDigestAndEventIndex::from_str(&format!("{}-{}", tx_digest, event_index)); + assert!(res.is_err()); + } + + #[test] + fn trimming_leading_ones_should_change_bytes() { + for _ in 0..100 { + let mut bytes = random_bytes(); + + // set a random (non-zero) number of leading bytes to 0 + let leading_zeroes = rand::random::() % bytes.len() + 1; + for b in bytes.iter_mut().take(leading_zeroes) { + *b = 0; + } + + let b58 = bs58::encode(&bytes).into_string(); + + // verify the base58 has the expected number of leading 1's + for c in b58.chars().take(leading_zeroes) { + assert_eq!(c, '1'); + } + + // trim a random (non-zero) number of leading 1's + let trim = rand::random::() % leading_zeroes + 1; + + // converting back to bytes should yield a different result + let decoded = bs58::decode(&b58[trim..]).into_vec().unwrap(); + assert_ne!(bytes.to_vec(), decoded); + } + } +} diff --git a/packages/axelar-wasm-std/src/msg_id/mod.rs b/packages/axelar-wasm-std/src/msg_id/mod.rs index 475c9b3b8..601837feb 100644 --- a/packages/axelar-wasm-std/src/msg_id/mod.rs +++ b/packages/axelar-wasm-std/src/msg_id/mod.rs @@ -4,10 +4,13 @@ use cosmwasm_schema::cw_serde; use error_stack::Report; use self::{ - base_58_event_index::Base58TxDigestAndEventIndex, tx_hash_event_index::HexTxHashAndEventIndex, + base_58_event_index::Base58TxDigestAndEventIndex, + base_58_solana_event_index::Base58SolanaTxDigestAndEventIndex, + tx_hash_event_index::HexTxHashAndEventIndex, }; pub mod base_58_event_index; +pub mod base_58_solana_event_index; pub mod tx_hash_event_index; #[derive(thiserror::Error)] @@ -39,6 +42,7 @@ pub trait MessageId: FromStr + Display {} pub enum MessageIdFormat { HexTxHashAndEventIndex, Base58TxDigestAndEventIndex, + Base58SolanaTxDigestAndEventIndex, } // function the router calls to verify msg ids @@ -50,6 +54,9 @@ pub fn verify_msg_id(message_id: &str, format: &MessageIdFormat) -> Result<(), R MessageIdFormat::Base58TxDigestAndEventIndex => { Base58TxDigestAndEventIndex::from_str(message_id).map(|_| ()) } + MessageIdFormat::Base58SolanaTxDigestAndEventIndex => { + Base58SolanaTxDigestAndEventIndex::from_str(message_id).map(|_| ()) + } } }