From 5bbcdf18e8b93cd5dd44ac0ab2a9e5b1a91020e1 Mon Sep 17 00:00:00 2001 From: CJ Cobb <46455409+cjcobb23@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:32:36 -0400 Subject: [PATCH] feat(multisig-prover): encode commands via bcs (#115) * feat(multisig-prover): encode commands via bcs --- Cargo.lock | 11 + contracts/multisig-prover/Cargo.toml | 1 + contracts/multisig-prover/src/encoding/abi.rs | 4 +- contracts/multisig-prover/src/encoding/bcs.rs | 224 ++++++++++++++++++ contracts/multisig-prover/src/encoding/mod.rs | 28 ++- contracts/multisig-prover/src/error.rs | 3 + contracts/multisig-prover/src/execute.rs | 4 +- 7 files changed, 267 insertions(+), 8 deletions(-) create mode 100644 contracts/multisig-prover/src/encoding/bcs.rs diff --git a/Cargo.lock b/Cargo.lock index 726cb56a7..4a84bdf9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -429,6 +429,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "bcs" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd3ffe8b19a604421a5d461d4a70346223e535903fbc3067138bddbebddcf77" +dependencies = [ + "serde", + "thiserror", +] + [[package]] name = "bech32" version = "0.9.1" @@ -3047,6 +3057,7 @@ dependencies = [ "anyhow", "axelar-wasm-std", "axelar-wasm-std-derive", + "bcs", "connection-router", "cosmwasm-schema", "cosmwasm-std", diff --git a/contracts/multisig-prover/Cargo.toml b/contracts/multisig-prover/Cargo.toml index 6ab434dd5..27d76c5d0 100644 --- a/contracts/multisig-prover/Cargo.toml +++ b/contracts/multisig-prover/Cargo.toml @@ -30,6 +30,7 @@ optimize = """docker run --rm -v "$(pwd)":/code \ [dependencies] axelar-wasm-std = { workspace = true } axelar-wasm-std-derive = { workspace = true } +bcs = "0.1.5" connection-router = { workspace = true, features = ["library"] } cosmwasm-schema = "1.1.3" cosmwasm-std = "1.1.3" diff --git a/contracts/multisig-prover/src/encoding/abi.rs b/contracts/multisig-prover/src/encoding/abi.rs index cfb952f40..0a296ba79 100644 --- a/contracts/multisig-prover/src/encoding/abi.rs +++ b/contracts/multisig-prover/src/encoding/abi.rs @@ -46,7 +46,7 @@ pub fn encode(data: &Data) -> HexBinary { .into() } -pub fn msg_to_sign(command_batch: &CommandBatch) -> HexBinary { +pub fn msg_digest(command_batch: &CommandBatch) -> HexBinary { let msg = Keccak256::digest(encode(&command_batch.data).as_slice()); // Prefix for standard EVM signed data https://eips.ethereum.org/EIPS/eip-191 @@ -614,7 +614,7 @@ mod test { encoder: Encoder::Abi, }; - let res = batch.msg_to_sign(); + let res = batch.msg_digest(); let expected_msg = test_data::msg_to_sign(); assert_eq!(res, expected_msg); diff --git a/contracts/multisig-prover/src/encoding/bcs.rs b/contracts/multisig-prover/src/encoding/bcs.rs new file mode 100644 index 000000000..b8bccd9a9 --- /dev/null +++ b/contracts/multisig-prover/src/encoding/bcs.rs @@ -0,0 +1,224 @@ +use bcs::to_bytes; +use cosmwasm_std::{HexBinary, Uint256}; +use itertools::Itertools; + +use crate::error::ContractError; + +use super::Data; + +// TODO: all of the public functions in this file should be moved to a trait, +// that has an abi and bcs implementation (and possibly others) + +pub fn command_params( + source_chain: String, + source_address: String, + destination_address: String, + payload_hash: HexBinary, +) -> Result { + if payload_hash.len() != 32 { + return Err(ContractError::InvalidMessage { + reason: format!("payload hash is not 32 bytes {}", payload_hash.to_hex()), + }); + } + + let destination_address = <[u8; 32]>::try_from( + HexBinary::from_hex(&destination_address)?.to_vec(), + ) + .map_err(|_| ContractError::InvalidMessage { + reason: format!( + "destination_address is not a valid Sui address: {}", + destination_address + ), + })?; + + Ok(to_bytes(&( + source_chain, + source_address, + destination_address, + payload_hash.to_vec(), + )) + .expect("couldn't serialize command as bcs") + .into()) +} + +fn u256_to_u64(chain_id: Uint256) -> u64 { + chain_id.to_string().parse().expect("chain_id is invalid") +} + +fn make_command_id(command_id: &HexBinary) -> [u8; 32] { + // command-ids are fixed length sequences + command_id + .to_vec() + .try_into() + .expect("couldn't convert command id to 32 byte array") +} + +pub fn encode(data: &Data) -> HexBinary { + // destination chain id must be u64 for sui + let destination_chain_id = u256_to_u64(data.destination_chain_id); + + let (commands_ids, command_types, command_params): (Vec<[u8; 32]>, Vec, Vec>) = + data.commands + .iter() + .map(|command| { + ( + make_command_id(&command.id), + command.ty.to_string(), + command.params.to_vec(), + ) + }) + .multiunzip(); + + to_bytes(&( + destination_chain_id, + commands_ids, + command_types, + command_params, + )) + .expect("couldn't encode batch as bcs") + .into() +} + +#[cfg(test)] +mod test { + + use std::vec; + + use bcs::from_bytes; + use cosmwasm_std::{HexBinary, Uint256}; + + use crate::{ + encoding::{ + bcs::{command_params, encode, make_command_id, u256_to_u64}, + Data, + }, + types::Command, + }; + + #[test] + fn test_chain_id_as_u64() { + let chain_id = 1u64; + assert_eq!(chain_id, u256_to_u64(Uint256::from(chain_id as u128))); + } + + #[test] + #[should_panic] + fn test_chain_id_as_u64_fails() { + let chain_id = u128::MAX; + u256_to_u64(Uint256::from(chain_id)); + } + + #[test] + fn test_make_command_id() { + assert_eq!([0; 32], make_command_id(&HexBinary::from(vec![0; 32]))); + } + + #[test] + #[should_panic] + fn test_make_command_id_fails_too_large() { + make_command_id(&HexBinary::from(vec![0; 30])); + } + + #[test] + fn test_command_params() { + let res = command_params( + "Ethereum".into(), + "00".into(), + "01".repeat(32).into(), + HexBinary::from_hex(&"02".repeat(32)).unwrap(), + ); + assert!(res.is_ok()); + + let res = res.unwrap(); + let params = from_bytes(&res.to_vec()); + assert!(params.is_ok()); + let (source_chain, source_address, destination_address, payload_hash): ( + String, + String, + [u8; 32], + Vec, + ) = params.unwrap(); + assert_eq!(source_chain, "Ethereum".to_string()); + + assert_eq!(source_address, "00".to_string()); + + assert_eq!( + destination_address.to_vec(), + HexBinary::from_hex(&"01".repeat(32)).unwrap().to_vec() + ); + + assert_eq!(payload_hash, vec![2; 32]); + } + + #[test] + fn test_invalid_destination_address() { + let res = command_params( + "Ethereum".into(), + "00".into(), + "01".into(), + HexBinary::from_hex("02").unwrap(), + ); + assert!(!res.is_ok()); + } + + #[test] + fn test_encode() { + let source_chain = "Ethereum"; + let source_address = "AA"; + let destination_address = "BB".repeat(32); + let payload_hash = HexBinary::from_hex(&"CC".repeat(32)).unwrap(); + let destination_chain_id = 1u64; + let command_id = HexBinary::from_hex(&"FF".repeat(32)).unwrap(); + let data = Data { + destination_chain_id: destination_chain_id.into(), + commands: vec![Command { + id: command_id.clone(), + ty: crate::types::CommandType::ApproveContractCall, + params: command_params( + source_chain.into(), + source_address.into(), + destination_address.clone().into(), + payload_hash.clone().into(), + ) + .unwrap(), + }], + }; + let encoded = encode(&data); + let decoded: Result<(u64, Vec<[u8; 32]>, Vec, Vec>), _> = + from_bytes(&encoded.to_vec()); + assert!(decoded.is_ok()); + let (chain_id, command_ids, command_types, params) = decoded.unwrap(); + + assert_eq!(chain_id, destination_chain_id); + + assert_eq!(command_ids.len(), 1); + assert_eq!(command_ids[0].to_vec(), command_id.to_vec()); + + assert_eq!(command_types.len(), 1); + assert_eq!( + command_types[0], + crate::types::CommandType::ApproveContractCall.to_string() + ); + + assert_eq!(params.len(), 1); + let command = from_bytes(¶ms[0]); + assert!(command.is_ok()); + let ( + source_chain_decoded, + source_address_decoded, + destination_address_decoded, + payload_hash_decoded, + ): (String, String, [u8; 32], Vec) = command.unwrap(); + + assert_eq!(source_chain_decoded, source_chain); + + assert_eq!(source_address_decoded, source_address); + + assert_eq!( + destination_address_decoded.to_vec(), + HexBinary::from_hex(&destination_address).unwrap().to_vec() + ); + + assert_eq!(payload_hash_decoded, payload_hash.to_vec()); + } +} diff --git a/contracts/multisig-prover/src/encoding/mod.rs b/contracts/multisig-prover/src/encoding/mod.rs index 4236e38a5..c6a9c03f3 100644 --- a/contracts/multisig-prover/src/encoding/mod.rs +++ b/contracts/multisig-prover/src/encoding/mod.rs @@ -1,4 +1,5 @@ mod abi; +mod bcs; use axelar_wasm_std::operators::Operators; use cosmwasm_schema::cw_serde; @@ -31,7 +32,12 @@ fn make_command(msg: Message, encoding: Encoder) -> Result todo!(), + Encoder::Bcs => bcs::command_params( + msg.source_chain, + msg.source_address, + msg.destination_address, + msg.payload_hash, + )?, }, id: command_id(msg.id), }) @@ -104,9 +110,9 @@ impl CommandBatchBuilder { } impl CommandBatch { - pub fn msg_to_sign(&self) -> HexBinary { + pub fn msg_digest(&self) -> HexBinary { match self.encoder { - Encoder::Abi => abi::msg_to_sign(self), + Encoder::Abi => abi::msg_digest(self), Encoder::Bcs => todo!(), } } @@ -133,7 +139,7 @@ impl Data { pub fn encode(&self, encoder: Encoder) -> HexBinary { match encoder { Encoder::Abi => abi::encode(self), - Encoder::Bcs => todo!(), + Encoder::Bcs => bcs::encode(self), } } } @@ -187,6 +193,20 @@ mod test { .unwrap() ); assert_eq!(res.ty, CommandType::ApproveContractCall); + + let mut router_message = router_message.to_owned(); + router_message.destination_address = "FF".repeat(32); + let res = make_command(router_message.to_owned(), Encoder::Bcs); + assert!(res.is_ok()); + + let res = res.unwrap(); + + assert_eq!( + res.id, + HexBinary::from_hex("3ee2f8af2201994e3518c9ce6848774785c2eef3bdbf9f954899497616dd59af") + .unwrap() + ); + assert_eq!(res.ty, CommandType::ApproveContractCall); } #[test] diff --git a/contracts/multisig-prover/src/error.rs b/contracts/multisig-prover/src/error.rs index e236b6025..e269465b3 100644 --- a/contracts/multisig-prover/src/error.rs +++ b/contracts/multisig-prover/src/error.rs @@ -35,6 +35,9 @@ pub enum ContractError { #[error(transparent)] NonEmptyError(#[from] nonempty::Error), + #[error(transparent)] + BcsError(#[from] bcs::Error), + #[error("worker set has not changed sufficiently since last update")] WorkerSetUnchanged, diff --git a/contracts/multisig-prover/src/execute.rs b/contracts/multisig-prover/src/execute.rs index b6c694d39..15eaa5243 100644 --- a/contracts/multisig-prover/src/execute.rs +++ b/contracts/multisig-prover/src/execute.rs @@ -50,7 +50,7 @@ pub fn construct_proof(deps: DepsMut, message_ids: Vec) -> Result Result