Skip to content

Commit

Permalink
support bech32m Message Id format
Browse files Browse the repository at this point in the history
  • Loading branch information
anstylian committed Nov 26, 2024
1 parent 7d715f8 commit a802126
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ axelar-wasm-std = { version = "^1.0.0", path = "packages/axelar-wasm-std" }
axelar-wasm-std-derive = { version = "^1.0.0", path = "packages/axelar-wasm-std-derive" }
axelarnet-gateway = { version = "^1.0.0", path = "contracts/axelarnet-gateway" }
bcs = "0.1.5"
bech32 = "0.11.0"
client = { version = "^1.0.0", path = "packages/client" }
coordinator = { version = "^1.1.0", path = "contracts/coordinator" }
cosmwasm-schema = "1.5.5"
Expand Down
1 change: 1 addition & 0 deletions contracts/voting-verifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ thiserror = { workspace = true }
[dev-dependencies]
alloy-primitives = { version = "0.7.7", features = ["getrandom"] }
assert_ok = { workspace = true }
bech32 = { workspace = true }
cw-multi-test = "0.15.1"
goldie = { workspace = true }
integration-tests = { workspace = true }
Expand Down
13 changes: 13 additions & 0 deletions contracts/voting-verifier/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ mod test {
assert_err_contains, err_contains, nonempty, MajorityThreshold, Threshold,
VerificationStatus,
};
use bech32::{Bech32m, Hrp};
use cosmwasm_std::testing::{
mock_dependencies, mock_env, mock_info, MockApi, MockQuerier, MockStorage,
};
Expand Down Expand Up @@ -265,6 +266,18 @@ mod test {
.to_string()
.parse()
.unwrap(),
MessageIdFormat::Bech32m {
prefix,
length: _length,
} => {
let data = format!("{id}-{index}");
let hrp = Hrp::parse(prefix).expect("valid hrp");
bech32::encode::<Bech32m>(hrp, data.as_bytes())
.unwrap()
.to_string()
.parse()
.unwrap()
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion contracts/voting-verifier/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::str::FromStr;
use std::vec::Vec;

use axelar_wasm_std::msg_id::{
Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, HexTxHash,
Base58SolanaTxSignatureAndEventIndex, Base58TxDigestAndEventIndex, Bech32mFormat, HexTxHash,
HexTxHashAndEventIndex, MessageIdFormat,
};
use axelar_wasm_std::voting::{PollId, Vote};
Expand Down Expand Up @@ -186,6 +186,11 @@ fn parse_message_id(

Ok((id.tx_hash_as_hex(), 0))
}
MessageIdFormat::Bech32m { prefix, length } => {
let bech32m_message_id = Bech32mFormat::from_str(prefix, *length, message_id)
.map_err(|_| ContractError::InvalidMessageID(message_id.into()))?;
Ok((bech32m_message_id.to_string().try_into()?, 0))
}
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/axelar-wasm-std/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \
[dependencies]
alloy-primitives = { workspace = true }
axelar-wasm-std-derive = { workspace = true, optional = true }
bech32 = { workspace = true }
bs58 = "0.5.1"
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
Expand Down
190 changes: 190 additions & 0 deletions packages/axelar-wasm-std/src/msg_id/bech32m.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use std::fmt::{self, Display};

use bech32::primitives::decode::CheckedHrpstring;
use bech32::Bech32m;
use error_stack::{bail, ensure, Report, ResultExt};
use regex::Regex;

use super::Error;

#[derive(Debug)]
pub struct Bech32mFormat {
pub encoded: String,
}

impl Bech32mFormat {
pub fn new(encoded: String) -> Self {
Self { encoded }
}

pub fn from_str(prefix: &str, length: usize, message_id: &str) -> Result<Self, Report<Error>> {
// The Bech32m prefix should be between 1 and 83 characters
ensure!(
!prefix.is_empty() && prefix.len() <= 83,
Error::InvalidBech32mLocalChecks("Prefix size should be between 1 and 83".to_string())
);

let data_part_length = length.saturating_sub(prefix.len()).saturating_sub(1);
ensure!(
data_part_length >= 6,
Error::InvalidBech32mLocalChecks(
"The data part should be at least 6 characters long".to_string()
)
);

let pattern = format!("^({prefix}1[02-9ac-hj-np-z]{{{data_part_length}}})$");

let regex = Regex::new(pattern.as_str()).change_context(
Error::InvalidBech32mLocalChecks("Failed to create regex".to_string()),
)?;

let (_, [string]) = regex
.captures(message_id)
.ok_or(Error::InvalidMessageID {
id: message_id.to_string(),
expected_format: format!("Bech32m with '{}' prefix", prefix),
})?
.extract();

verify_bech32m(string, prefix)?;

Ok(Self {
encoded: string.to_string(),
})
}
}

impl Display for Bech32mFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.encoded)
}
}

fn verify_bech32m(input: &str, expected_prefix: &str) -> Result<(), Report<Error>> {
let checked_bech32m = CheckedHrpstring::new::<Bech32m>(input)
.change_context(Error::InvalidBech32mExternalChecks(input.to_string()))?;

ensure!(
checked_bech32m.hrp().as_str() == expected_prefix,
Error::InvalidBech32mExternalChecks(format!(
"Expected prefix '{expected_prefix}' not found: '{input}'"
))
);

if checked_bech32m.data_part_ascii_no_checksum().is_empty() {
bail!(Error::InvalidBech32mExternalChecks(format!(
"Message Id is missing the data part: '{input}'"
)));
}

Ok(())
}

#[cfg(test)]
mod test {
use bech32::Hrp;
use rand::Rng;

use crate::assert_err_contains;

use super::*;

#[test]
fn should_pass_bech32m() {
let mut rng = rand::thread_rng();

const CHARS: [char; 32] = [
'q', 'p', 'z', 'r', 'y', '9', 'x', '8', 'g', 'f', '2', 't', 'v', 'd', 'w', '0', 's',
'3', 'j', 'n', '5', '4', 'k', 'h', 'c', 'e', '6', 'm', 'u', 'a', '7', 'l',
];
let char_set = CHARS.len();

for _ in 0..100 {
let hrp_str = (0..rng.gen_range(1..=83))
.map(|_| CHARS[rng.gen_range(0..char_set)])
.collect::<String>();

let data = (0..80)
.map(|_| char::from(rng.gen_range(32..=126)))
.collect::<String>();

let hrp = Hrp::parse(hrp_str.as_str()).expect("valid hrp");
let string =
bech32::encode::<Bech32m>(hrp, data.as_bytes()).expect("failed to encode string");

assert!(Bech32mFormat::from_str(hrp.as_str(), string.len(), string.as_str()).is_ok());
}
}

#[test]
fn should_pass_edge_cases() {
let mut rng = rand::thread_rng();
let data = (0..80)
.map(|_| char::from(rng.gen_range(32..=126)))
.collect::<String>();

// Minimum prefix length
let hrp_str = "a";
let hrp = Hrp::parse(hrp_str).expect("valid hrp");
let string =
bech32::encode::<Bech32m>(hrp, data.as_bytes()).expect("failed to encode string");

assert!(Bech32mFormat::from_str(hrp.as_str(), string.len(), string.as_str()).is_ok());

// Maximum prefix length
let hrp_string = "a".repeat(83);
let hrp = Hrp::parse(hrp_string.as_str()).expect("valid hrp");
let string =
bech32::encode::<Bech32m>(hrp, data.as_bytes()).expect("failed to encode string");
assert!(Bech32mFormat::from_str(hrp.as_str(), string.len(), string.as_str()).is_ok());
}

#[test]
fn should_fail_with_invalid_message_id() {
let string = "at1hs0xk375g4kvw53rcem9nyjsdw5lsv94fl065n77cpt0774nsyysdecaju";
let hrp = "at";

assert_err_contains!(
Bech32mFormat::from_str(hrp, string.len() + 1, string),
Error,
Error::InvalidMessageID { .. }
);

assert_err_contains!(
Bech32mFormat::from_str(hrp, string.len() - 1, string),
Error,
Error::InvalidMessageID { .. }
);

assert_err_contains!(
Bech32mFormat::from_str("au", string.len(), string),
Error,
Error::InvalidMessageID { .. }
);
}

#[test]
fn should_not_pass_empty_data_part() {
let hrp_string = "a";
let hrp = Hrp::parse(hrp_string).expect("valid hrp");
let string = "a1";
assert_err_contains!(
Bech32mFormat::from_str(hrp.as_str(), string.len(), string),
Error,
Error::InvalidBech32mLocalChecks(..)
);

// Minimum data part length
let data = "";
let hrp_string = "a";
let hrp = Hrp::parse(hrp_string).expect("valid hrp");
let string =
bech32::encode::<Bech32m>(hrp, data.as_bytes()).expect("failed to encode string");

assert_err_contains!(
Bech32mFormat::from_str(hrp.as_str(), string.len(), string.as_str()),
Error,
Error::InvalidBech32mExternalChecks(..)
);
}
}
46 changes: 46 additions & 0 deletions packages/axelar-wasm-std/src/msg_id/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ 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::tx_hash::HexTxHash;
pub use self::tx_hash_event_index::HexTxHashAndEventIndex;

mod base_58_event_index;
mod base_58_solana_event_index;
mod bech32m;
mod tx_hash;
mod tx_hash_event_index;

Expand All @@ -25,6 +27,10 @@ pub enum Error {
InvalidTxHash(String),
#[error("invalid tx digest in message id '{0}'")]
InvalidTxDigest(String),
#[error("Invalid bech32m: '{0}'")]
InvalidBech32mLocalChecks(String),
#[error("Invalid bech32m: '{0}'")]
InvalidBech32mExternalChecks(String),
}

/// Any message id format must implement this trait.
Expand All @@ -45,6 +51,7 @@ pub enum MessageIdFormat {
Base58TxDigestAndEventIndex,
Base58SolanaTxSignatureAndEventIndex,
HexTxHash,
Bech32m { prefix: String, length: usize },
}

// function the router calls to verify msg ids
Expand All @@ -60,6 +67,9 @@ pub fn verify_msg_id(message_id: &str, format: &MessageIdFormat) -> Result<(), R
Base58SolanaTxSignatureAndEventIndex::from_str(message_id).map(|_| ())
}
MessageIdFormat::HexTxHash => HexTxHash::from_str(message_id).map(|_| ()),
MessageIdFormat::Bech32m { prefix, length } => {
Bech32mFormat::from_str(prefix, *length, message_id).map(|_| ())
}
}
}

Expand Down Expand Up @@ -111,4 +121,40 @@ mod test {
.to_string();
assert!(verify_msg_id(&msg_id, &MessageIdFormat::HexTxHashAndEventIndex).is_err());
}

#[test]
fn should_verify_bech32m() {
let message_id = "at1hs0xk375g4kvw53rcem9nyjsdw5lsv94fl065n77cpt0774nsyysdecaju";
assert!(verify_msg_id(
message_id,
&MessageIdFormat::Bech32m {
prefix: "at".to_string(),
length: 61
}
)
.is_ok());
}

#[test]
fn should_not_verify_bech32m() {
let message_id = "aths0xk375g4kvw53rcem9nyjsdw5lsv94fl065n77cpt0774nsyysdecaju";
assert!(verify_msg_id(
message_id,
&MessageIdFormat::Bech32m {
prefix: "at".to_string(),
length: 61
}
)
.is_err());

let message_id = "ath1s0xk375g4kvw53rcem9nyjsdw5lsv94fl065n77cpt0774nsyysdecaj";
assert!(verify_msg_id(
message_id,
&MessageIdFormat::Bech32m {
prefix: "at".to_string(),
length: 61
}
)
.is_err());
}
}

0 comments on commit a802126

Please sign in to comment.