diff --git a/Cargo.lock b/Cargo.lock index 49dc83e38..d5854b923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,7 @@ version = "0.10.0" dependencies = [ "cosmwasm-std", "cw-storage-plus", + "prost", "schemars", "serde", "thiserror", @@ -914,9 +915,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5e2533f59d08fcf364fd374ebda0692a70bd6d7e66ef97f306f45c6c5d8020" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ "bytes", "prost-derive", @@ -924,9 +925,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", "itertools", diff --git a/packages/cw0/Cargo.toml b/packages/cw0/Cargo.toml index e6717137a..f54307163 100644 --- a/packages/cw0/Cargo.toml +++ b/packages/cw0/Cargo.toml @@ -19,3 +19,4 @@ thiserror = { version = "1.0.21" } [dev-dependencies] cw-storage-plus = { path = "../../packages/storage-plus", version = "0.10.0" } +prost = "0.9" diff --git a/packages/cw0/src/lib.rs b/packages/cw0/src/lib.rs index fbb000906..d84e61a3f 100644 --- a/packages/cw0/src/lib.rs +++ b/packages/cw0/src/lib.rs @@ -2,11 +2,13 @@ mod balance; mod event; mod expiration; mod pagination; +mod parse_reply; mod payment; pub use pagination::{ calc_range_end, calc_range_start, calc_range_start_string, maybe_addr, maybe_canonical, }; +pub use parse_reply::{parse_reply_execute_data, parse_reply_instantiate_data}; pub use payment::{may_pay, must_pay, nonpayable, one_coin, PaymentError}; pub use crate::balance::NativeBalance; diff --git a/packages/cw0/src/parse_reply.rs b/packages/cw0/src/parse_reply.rs new file mode 100644 index 000000000..2778a4a68 --- /dev/null +++ b/packages/cw0/src/parse_reply.rs @@ -0,0 +1,517 @@ +use thiserror::Error; + +use cosmwasm_std::{Binary, Reply}; + +// Protobuf wire types (https://developers.google.com/protocol-buffers/docs/encoding) +const WIRE_TYPE_LENGTH_DELIMITED: u8 = 2; +// Up to 9 bytes of varints as a practical limit (https://github.com/multiformats/unsigned-varint#practical-maximum-of-9-bytes-for-security) +const VARINT_MAX_BYTES: usize = 9; + +#[derive(Clone, Debug, PartialEq)] +pub struct MsgInstantiateContractResponse { + pub contract_address: String, + pub data: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct MsgExecuteContractResponse { + pub data: Option, +} + +/// Base128 varint decoding. +/// The remaining of the data is kept in the data parameter. +fn parse_protobuf_varint(data: &mut Vec, field_number: u8) -> Result { + let data_len = data.len(); + let mut len: u64 = 0; + let mut i = 0; + while i < VARINT_MAX_BYTES { + if data_len == i { + return Err(ParseReplyError::ParseFailure(format!( + "failed to decode Protobuf message: field #{}: varint data too short", + field_number + ))); + } + len += ((data[i] & 0x7f) as u64) << (i * 7); + if data[i] & 0x80 == 0 { + break; + } + i += 1; + } + if i == VARINT_MAX_BYTES { + return Err(ParseReplyError::ParseFailure(format!( + "failed to decode Protobuf message: field #{}: varint data too long", + field_number + ))); + } + *data = data[i + 1..].to_owned(); + + Ok(len as usize) // Gently fall back to the arch's max addressable size +} + +/// Helper function to parse length-prefixed protobuf fields. +/// The remaining of the data is kept in the data parameter. +fn parse_protobuf_length_prefixed( + data: &mut Vec, + field_number: u8, +) -> Result, ParseReplyError> { + if data.is_empty() { + return Ok(vec![]); + }; + let mut rest_1 = data.split_off(1); + let wire_type = data[0] & 0b11; + let field = data[0] >> 3; + + if field != field_number { + return Err(ParseReplyError::ParseFailure(format!( + "failed to decode Protobuf message: invalid field #{} for field #{}", + field, field_number + ))); + } + if wire_type != WIRE_TYPE_LENGTH_DELIMITED { + return Err(ParseReplyError::ParseFailure(format!( + "failed to decode Protobuf message: field #{}: invalid wire type {}", + field_number, wire_type + ))); + } + + let len = parse_protobuf_varint(&mut rest_1, field_number)?; + if rest_1.len() < len { + return Err(ParseReplyError::ParseFailure(format!( + "failed to decode Protobuf message: field #{}: message too short", + field_number + ))); + } + *data = rest_1.split_off(len); + + Ok(rest_1) +} + +fn parse_protobuf_string(data: &mut Vec, field_number: u8) -> Result { + let str_field = parse_protobuf_length_prefixed(data, field_number)?; + Ok(String::from_utf8(str_field)?) +} + +fn parse_protobuf_bytes( + data: &mut Vec, + field_number: u8, +) -> Result, ParseReplyError> { + let bytes_field = parse_protobuf_length_prefixed(data, field_number)?; + if bytes_field.is_empty() { + Ok(None) + } else { + Ok(Some(Binary(bytes_field))) + } +} + +pub fn parse_reply_instantiate_data( + msg: Reply, +) -> Result { + let data = msg + .result + .into_result() + .map_err(ParseReplyError::SubMsgFailure)? + .data + .ok_or_else(|| ParseReplyError::ParseFailure("Missing reply data".to_owned()))?; + // Manual protobuf decoding + let mut data = data.0; + // Parse contract addr + let contract_addr = parse_protobuf_string(&mut data, 1)?; + + // Parse (optional) data + let data = parse_protobuf_bytes(&mut data, 2)?; + + Ok(MsgInstantiateContractResponse { + contract_address: contract_addr, + data, + }) +} + +pub fn parse_reply_execute_data(msg: Reply) -> Result { + let data = msg + .result + .into_result() + .map_err(ParseReplyError::SubMsgFailure)? + .data + .ok_or_else(|| ParseReplyError::ParseFailure("Missing reply data".to_owned()))?; + // Manual protobuf decoding + let mut data = data.0; + // Parse (optional) data + let data = parse_protobuf_bytes(&mut data, 1)?; + + Ok(MsgExecuteContractResponse { data }) +} + +#[derive(Error, Debug, PartialEq)] +pub enum ParseReplyError { + #[error("Failure response from sub-message: {0}")] + SubMsgFailure(String), + + #[error("Invalid reply from sub-message: {0}")] + ParseFailure(String), + + #[error("Error occurred while converting from UTF-8")] + BrokenUtf8(#[from] std::string::FromUtf8Error), +} + +#[cfg(test)] +mod test { + use super::*; + use crate::parse_reply::ParseReplyError::{BrokenUtf8, ParseFailure}; + use cosmwasm_std::{ContractResult, SubMsgExecutionResponse}; + use prost::Message; + use std::str::from_utf8; + + fn encode_bytes(data: &[u8]) -> Vec { + #[derive(Clone, PartialEq, Message)] + struct ProtobufBytes { + #[prost(bytes, tag = "1")] + pub data: Vec, + } + + let data = ProtobufBytes { + data: data.to_vec(), + }; + let mut encoded_data = Vec::::with_capacity(data.encoded_len()); + data.encode(&mut encoded_data).unwrap(); + + encoded_data + } + + fn encode_string(data: &str) -> Vec { + #[derive(Clone, PartialEq, Message)] + struct ProtobufString { + #[prost(string, tag = "1")] + pub data: String, + } + + let data = ProtobufString { + data: data.to_string(), + }; + let mut encoded_data = Vec::::with_capacity(data.encoded_len()); + data.encode(&mut encoded_data).unwrap(); + + encoded_data + } + + #[derive(Clone, PartialEq, Message)] + struct MsgInstantiateContractResponse { + #[prost(string, tag = "1")] + pub contract_address: ::prost::alloc::string::String, + #[prost(bytes, tag = "2")] + pub data: ::prost::alloc::vec::Vec, + } + + #[derive(Clone, PartialEq, Message)] + struct MsgExecuteContractResponse { + #[prost(bytes, tag = "1")] + pub data: ::prost::alloc::vec::Vec, + } + + #[test] + fn parse_protobuf_varint_tests() { + let field_number = 1; + // Single-byte varint works + let mut data = b"\x0a".to_vec(); + let len = parse_protobuf_varint(&mut data, field_number).unwrap(); + assert_eq!(len, 10); + + // Rest is returned + let mut data = b"\x0a\x0b".to_vec(); + let len = parse_protobuf_varint(&mut data, field_number).unwrap(); + assert_eq!(len, 10); + assert_eq!(data, b"\x0b".to_vec()); + + // Multi-byte varint works + // 300 % 128 = 44. 44 + 128 = 172 (0xac) (1st byte) + // 300 / 128 = 2 (x02) (2nd byte) + let mut data = b"\xac\x02".to_vec(); + let len = parse_protobuf_varint(&mut data, field_number).unwrap(); + assert_eq!(len, 300); + + // Rest is returned + let mut data = b"\xac\x02\x0c".to_vec(); + let len = parse_protobuf_varint(&mut data, field_number).unwrap(); + assert_eq!(len, 300); + assert_eq!(data, b"\x0c".to_vec()); + + // varint data too short (Empty varint) + let mut data = vec![]; + let err = parse_protobuf_varint(&mut data, field_number).unwrap_err(); + assert!(matches!(err, ParseFailure(..))); + + // varint data too short (Incomplete varint) + let mut data = b"\x80".to_vec(); + let err = parse_protobuf_varint(&mut data, field_number).unwrap_err(); + assert!(matches!(err, ParseFailure(..))); + + // varint data too long + let mut data = b"\x80\x81\x82\x83\x84\x83\x82\x81\x80".to_vec(); + let err = parse_protobuf_varint(&mut data, field_number).unwrap_err(); + assert!(matches!(err, ParseFailure(..))); + } + + #[test] + fn parse_protobuf_length_prefixed_tests() { + let field_number = 1; + // Single-byte length-prefixed works + let mut data = b"\x0a\x03abc".to_vec(); + let res = parse_protobuf_length_prefixed(&mut data, field_number).unwrap(); + assert_eq!(res, b"abc".to_vec()); + assert_eq!(data, vec![0u8; 0]); + + // Rest is returned + let mut data = b"\x0a\x03abcd".to_vec(); + let res = parse_protobuf_length_prefixed(&mut data, field_number).unwrap(); + assert_eq!(res, b"abc".to_vec()); + assert_eq!(data, b"d".to_vec()); + + // Multi-byte length-prefixed works + let mut data = [b"\x0a\xac\x02", vec![65u8; 300].as_slice()] + .concat() + .to_vec(); + let res = parse_protobuf_length_prefixed(&mut data, field_number).unwrap(); + assert_eq!(res, vec![65u8; 300]); + assert_eq!(data, vec![0u8; 0]); + + // Rest is returned + let mut data = [b"\x0a\xac\x02", vec![65u8; 300].as_slice(), b"rest"] + .concat() + .to_vec(); + let res = parse_protobuf_length_prefixed(&mut data, field_number).unwrap(); + assert_eq!(res, vec![65u8; 300]); + assert_eq!(data, b"rest"); + + // message too short + let mut data = b"\x0a\x01".to_vec(); + let field_number = 1; + let err = parse_protobuf_length_prefixed(&mut data, field_number).unwrap_err(); + assert!(matches!(err, ParseFailure(..))); + + // invalid wire type + let mut data = b"\x0b\x01a".to_vec(); + let err = parse_protobuf_length_prefixed(&mut data, field_number).unwrap_err(); + assert!(matches!(err, ParseFailure(..))); + + // invalid field number + let field_number = 2; + let mut data = b"\x0a\x01a".to_vec(); + let err = parse_protobuf_length_prefixed(&mut data, field_number).unwrap_err(); + assert!(matches!(err, ParseFailure(..))); + } + + #[test] + fn parse_protobuf_bytes_works() { + let field_number = 1; + + // Empty works + let data = vec![]; + let mut encoded_data = encode_bytes(&data); + + let res = parse_protobuf_bytes(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, None); + + // Simple works + let data = b"test".to_vec(); + let mut encoded_data = encode_bytes(&data); + + let res = parse_protobuf_bytes(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, Some(Binary(data))); + + // Large works + let data = vec![0x40; 300]; + let mut encoded_data = encode_bytes(&data); + + let res = parse_protobuf_bytes(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, Some(Binary(data))); + + // Field number works + let field_number = 5; + let data = b"test field 5".to_vec(); + let mut encoded_data = encode_bytes(&data); + encoded_data[0] = (field_number << 3) + WIRE_TYPE_LENGTH_DELIMITED; + + let res = parse_protobuf_bytes(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, Some(Binary(data))); + + // Remainder is kept + let field_number = 1; + let test_len: usize = 4; + let data = b"test_remainder".to_vec(); + let mut encoded_data = encode_bytes(&data); + encoded_data[1] = test_len as u8; + + let res = parse_protobuf_bytes(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, Some(Binary(data[..test_len].to_owned()))); + assert_eq!(encoded_data, data[test_len..].to_owned()); + } + + #[test] + fn parse_protobuf_string_tests() { + let field_number = 1; + + // Empty works + let data = ""; + let mut encoded_data = encode_string(data); + + let res = parse_protobuf_string(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, data); + + // Simple works + let data = "test"; + let mut encoded_data = encode_string(data); + + let res = parse_protobuf_string(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, data); + + // Large works + let data = vec![0x40; 300]; + let str_data = from_utf8(data.as_slice()).unwrap(); + let mut encoded_data = encode_string(str_data); + + let res = parse_protobuf_string(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, str_data); + + // Field number works + let field_number = 5; + let data = "test field 5"; + let mut encoded_data = encode_string(data); + encoded_data[0] = (field_number << 3) + WIRE_TYPE_LENGTH_DELIMITED; + + let res = parse_protobuf_string(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, data); + + // Remainder is kept + let field_number = 1; + let test_len: usize = 4; + let data = "test_remainder"; + let mut encoded_data = encode_string(data); + encoded_data[1] = test_len as u8; + + let res = parse_protobuf_string(&mut encoded_data, field_number).unwrap(); + assert_eq!(res, data[..test_len]); + assert_eq!(encoded_data, data[test_len..].as_bytes()); + + // Broken utf-8 errs + let field_number = 1; + let data = "test_X"; + let mut encoded_data = encode_string(data); + let encoded_len = encoded_data.len(); + encoded_data[encoded_len - 1] = 0xd3; + let err = parse_protobuf_string(&mut encoded_data, field_number).unwrap_err(); + assert!(matches!(err, BrokenUtf8(..))); + } + + #[test] + fn parse_reply_instantiate_data_works() { + let contract_addr: &str = "Contract #1"; + for (data, expected) in [ + ( + vec![], + super::MsgInstantiateContractResponse { + contract_address: contract_addr.to_string(), + data: None, + }, + ), + ( + vec![1u8, 2, 255, 7, 5], + super::MsgInstantiateContractResponse { + contract_address: contract_addr.to_string(), + data: Some(Binary(vec![1u8, 2, 255, 7, 5])), + }, + ), + ( + vec![1u8; 127], + super::MsgInstantiateContractResponse { + contract_address: contract_addr.to_string(), + data: Some(Binary(vec![1u8; 127])), + }, + ), + ( + vec![2u8; 128], + super::MsgInstantiateContractResponse { + contract_address: contract_addr.to_string(), + data: Some(Binary(vec![2u8; 128])), + }, + ), + ( + vec![3u8; 257], + super::MsgInstantiateContractResponse { + contract_address: contract_addr.to_string(), + data: Some(Binary(vec![3u8; 257])), + }, + ), + ] { + let instantiate_reply = MsgInstantiateContractResponse { + contract_address: contract_addr.to_string(), + data, + }; + let mut encoded_instantiate_reply = + Vec::::with_capacity(instantiate_reply.encoded_len()); + // The data must encode successfully + instantiate_reply + .encode(&mut encoded_instantiate_reply) + .unwrap(); + + // Build reply message + let msg = Reply { + id: 1, + result: ContractResult::Ok(SubMsgExecutionResponse { + events: vec![], + data: Some(encoded_instantiate_reply.into()), + }), + }; + + let res = parse_reply_instantiate_data(msg).unwrap(); + assert_eq!(res, expected); + } + } + + #[test] + fn parse_reply_execute_data_works() { + for (data, expected) in [ + (vec![], super::MsgExecuteContractResponse { data: None }), + ( + vec![1u8, 2, 3, 127, 15], + super::MsgExecuteContractResponse { + data: Some(Binary(vec![1u8, 2, 3, 127, 15])), + }, + ), + ( + vec![0u8; 255], + super::MsgExecuteContractResponse { + data: Some(Binary(vec![0u8; 255])), + }, + ), + ( + vec![1u8; 256], + super::MsgExecuteContractResponse { + data: Some(Binary(vec![1u8; 256])), + }, + ), + ( + vec![2u8; 32769], + super::MsgExecuteContractResponse { + data: Some(Binary(vec![2u8; 32769])), + }, + ), + ] { + let execute_reply = MsgExecuteContractResponse { data }; + let mut encoded_execute_reply = Vec::::with_capacity(execute_reply.encoded_len()); + // The data must encode successfully + execute_reply.encode(&mut encoded_execute_reply).unwrap(); + + // Build reply message + let msg = Reply { + id: 1, + result: ContractResult::Ok(SubMsgExecutionResponse { + events: vec![], + data: Some(encoded_execute_reply.into()), + }), + }; + + let res = parse_reply_execute_data(msg).unwrap(); + + assert_eq!(res, expected); + } + } +} diff --git a/packages/multi-test/Cargo.toml b/packages/multi-test/Cargo.toml index 734ad221a..bae1b4325 100644 --- a/packages/multi-test/Cargo.toml +++ b/packages/multi-test/Cargo.toml @@ -25,7 +25,7 @@ cosmwasm-storage = { version = "1.0.0-beta" } itertools = "0.10.1" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -prost = "0.8.0" +prost = "0.9" anyhow = "1" thiserror = "1" derivative = "2"