From 087c427d7f4adf6de427c618aa42c4672d6ddeb6 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sat, 25 Jan 2020 12:34:36 -0800 Subject: [PATCH 1/5] cosmos-stdtx: Initial crate with sdk.Msg schema support Initial crate boilerplate + schema types (and TOML definition language) for describing `sdk.Msg` types. --- Cargo.lock | 11 +++ Cargo.toml | 6 +- cosmos-stdtx/Cargo.toml | 21 +++++ cosmos-stdtx/README.md | 58 ++++++++++++ cosmos-stdtx/src/error.rs | 69 ++++++++++++++ cosmos-stdtx/src/lib.rs | 18 ++++ cosmos-stdtx/src/schema.rs | 83 +++++++++++++++++ cosmos-stdtx/src/schema/definition.rs | 40 +++++++++ cosmos-stdtx/src/schema/field.rs | 90 +++++++++++++++++++ cosmos-stdtx/src/schema/type_name.rs | 78 ++++++++++++++++ cosmos-stdtx/src/schema/value_type.rs | 63 +++++++++++++ cosmos-stdtx/tests/integration.rs | 19 ++++ .../tests/support/example_schema.toml | 28 ++++++ 13 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 cosmos-stdtx/Cargo.toml create mode 100644 cosmos-stdtx/README.md create mode 100644 cosmos-stdtx/src/error.rs create mode 100644 cosmos-stdtx/src/lib.rs create mode 100644 cosmos-stdtx/src/schema.rs create mode 100644 cosmos-stdtx/src/schema/definition.rs create mode 100644 cosmos-stdtx/src/schema/field.rs create mode 100644 cosmos-stdtx/src/schema/type_name.rs create mode 100644 cosmos-stdtx/src/schema/value_type.rs create mode 100644 cosmos-stdtx/tests/integration.rs create mode 100644 cosmos-stdtx/tests/support/example_schema.toml diff --git a/Cargo.lock b/Cargo.lock index cf63d5c..6d62a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,17 @@ dependencies = [ "termcolor", ] +[[package]] +name = "cosmos-stdtx" +version = "0.0.1" +dependencies = [ + "anomaly", + "serde", + "sha2", + "thiserror", + "toml", +] + [[package]] name = "crc32fast" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 903a89b..1d22de6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,16 @@ description = "Tendermint Key Management System" version = "0.7.1" # Also update html_root_url in lib.rs when bumping this authors = ["Tony Arcieri ", "Ismail Khoffi "] license = "Apache-2.0" -homepage = "https://github.com/tendermint/kms/" +homepage = "https://tendermint.com/" +repository = "https://github.com/tendermint/kms/" readme = "README.md" categories = ["cryptography"] keywords = ["cosmos", "ed25519", "kms", "key-management", "yubihsm"] edition = "2018" +[workspace] +members = [".", "cosmos-stdtx"] + [badges] circle-ci = { repository = "tendermint/kms" } diff --git a/cosmos-stdtx/Cargo.toml b/cosmos-stdtx/Cargo.toml new file mode 100644 index 0000000..d2ffb8c --- /dev/null +++ b/cosmos-stdtx/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cosmos-stdtx" +description = "Extensible schema-driven Cosmos StdTx builder and serializer" +version = "0.0.1" # Also update html_root_url in lib.rs when bumping this +authors = ["Tony Arcieri "] +license = "Apache-2.0" +repository = "https://github.com/tendermint/kms/tree/master/cosmos-stdtx" +readme = "README.md" +categories = ["cryptography", "encoding"] +keywords = ["crypto", "cosmos", "stdtx", "transaction", "tendermint"] +edition = "2018" + +[badges] +circle-ci = { repository = "tendermint/kms" } + +[dependencies] +anomaly = "0.1" +serde = { version = "1", features = ["serde_derive"] } +sha2 = "0.8" +thiserror = "1" +toml = "0.5" diff --git a/cosmos-stdtx/README.md b/cosmos-stdtx/README.md new file mode 100644 index 0000000..6832092 --- /dev/null +++ b/cosmos-stdtx/README.md @@ -0,0 +1,58 @@ +# cosmos-stdtx.rs 🌌 + +[![Crate][crate-image]][crate-link] +[![Build Status][build-image]][build-link] +[![Apache 2.0 Licensed][license-image]][license-link] +![MSRV][rustc-image] + +Extensible schema-driven [Cosmos] [StdTx] builder and serializer. + +## About + +**cosmos-stdtx.rs** is a Rust library for composing transactions in the [StdTx] +format used by several [Tendermint]-based networks. + +It includes support for cryptographically signing transactions and serializing +them in the [Amino] encoding format. + +Definitions of transaction types are easily extensible, and can be defined at +runtime by loading them from a TOML definition file. This allows +**cosmos-stdtx.rs** to be used with any [Tendermint]-based software which +uses the [StdTx] format without requiring upstream modifications. + +## Minimum Supported Rust Version + +- Rust **1.39+** + +## License + +Copyright © 2020 Tony Arcieri + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +[//]: # (badges) + +[crate-image]: https://img.shields.io/crates/v/cosmos-stdtx.svg +[crate-link]: https://crates.io/crates/cosmos-stdtx +[build-image]: https://circleci.com/gh/tendermint/kms.svg?style=shield +[build-link]: https://circleci.com/gh/tendermint/kms +[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg +[license-link]: https://github.com/tendermint/kms/blob/master/LICENSE +[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg + +[//]: # (general links) + +[Cosmos]: https://cosmos.network/ +[StdTx]: https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx +[Tendermint]: https://tendermint.com/ +[Amino]: https://github.com/tendermint/go-amino diff --git a/cosmos-stdtx/src/error.rs b/cosmos-stdtx/src/error.rs new file mode 100644 index 0000000..7cdb879 --- /dev/null +++ b/cosmos-stdtx/src/error.rs @@ -0,0 +1,69 @@ +//! Error types + +use anomaly::{BoxError, Context}; +use std::{ + fmt::{self, Display}, + ops::Deref, +}; +use thiserror::Error; + +/// Kinds of errors +#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] +pub enum ErrorKind { + /// Input/output errors + #[error("I/O error")] + Io, + + /// Parse error + #[error("parse error")] + Parse, +} + +impl ErrorKind { + /// Add context to an [`ErrorKind`] + pub fn context(self, source: impl Into) -> Context { + Context::new(self, Some(source.into())) + } +} + +/// Error type +#[derive(Debug)] +pub struct Error(Box>); + +impl Deref for Error { + type Target = Context; + + fn deref(&self) -> &Context { + &self.0 + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + self.0.source() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Context::new(kind, None).into() + } +} + +impl From> for Error { + fn from(context: Context) -> Self { + Error(Box::new(context)) + } +} + +impl From for Error { + fn from(source: toml::de::Error) -> Error { + Context::new(ErrorKind::Parse, Some(source.into())).into() + } +} diff --git a/cosmos-stdtx/src/lib.rs b/cosmos-stdtx/src/lib.rs new file mode 100644 index 0000000..a7f7cc9 --- /dev/null +++ b/cosmos-stdtx/src/lib.rs @@ -0,0 +1,18 @@ +//! Amino serializer for Cosmos SDK-formatted `StdTx` transactions. +//! +//! This serializer accepts a TOML-based `sdk.Msg` schema as input, along +//! with a JSON +//! +//! # Equivalent Go code +//! +//! - [`StdTx` (godoc)](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx) +//! - [`sdk.Msg` (godoc)](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg) + +#![doc(html_root_url = "https://docs.rs/cosmos-stdtx/0.0.1")] +#![forbid(unsafe_code)] +#![warn(rust_2018_idioms, missing_docs, unused_qualifications)] + +pub mod error; +pub mod schema; + +pub use self::{error::Error, schema::Schema}; diff --git a/cosmos-stdtx/src/schema.rs b/cosmos-stdtx/src/schema.rs new file mode 100644 index 0000000..fce2bad --- /dev/null +++ b/cosmos-stdtx/src/schema.rs @@ -0,0 +1,83 @@ +//! Amino schema for an `sdk.Msg`. +//! +//! Schema files are similar to Protobuf schemas, but use a TOML-based syntax. +//! +//! # Example TOML File +//! +//! Below is an example TOML file defining an `sdk.Msg`. This example defines +//! a type named `oracle/MsgExchangeRatePrevote`: +//! +//! ```toml +//! [[definition]] +//! type_name = "oracle/MsgExchangeRatePrevote" +//! fields = [ +//! { name = "hash", type = "string", tag = "1" }, # tag will be inferred if unspecified +//! { name = "denom", type = "string" }, +//! { name = "feeder", type = "sdk.AccAddress" }, +//! { name = "validator", type = "sdk.ValAddress" }, +//! ] +//! ``` + +mod definition; +mod field; +mod type_name; +mod value_type; + +pub use self::{definition::Definition, field::Field, type_name::TypeName, value_type::ValueType}; + +use crate::error::{Error, ErrorKind}; +use anomaly::fail; +use serde::Deserialize; +use std::{fs, path::Path, str::FromStr}; + +/// Schema definition for an [`sdk.Msg`] to be included in an [`StdTx`]. +/// +/// The schema includes information about field identifiers and associated types. +/// +/// [`StdTx`]: https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx +/// [`sdk.Msg`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct Schema { + /// `StdTx` namespace for schema + namespace: TypeName, + + /// Schema definitions + #[serde(rename = "definition")] + definitions: Vec, +} + +impl Schema { + /// Create a new [`Schema`] with the given `StdTx` namespace and [`Definition`] set + pub fn new(namespace: TypeName, definitions: impl Into>) -> Self { + Self { + namespace, + definitions: definitions.into(), + } + } + + /// Load a TOML file describing + pub fn load_toml(path: impl AsRef) -> Result { + match fs::read_to_string(path.as_ref()) { + Ok(s) => s.parse(), + Err(e) => fail!( + ErrorKind::Io, + "couldn't open {}: {}", + path.as_ref().display(), + e + ), + } + } + + /// [`Definition`] types found in this [`Schema`] + pub fn definitions(&self) -> &[Definition] { + &self.definitions + } +} + +impl FromStr for Schema { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(toml::from_str(s)?) + } +} diff --git a/cosmos-stdtx/src/schema/definition.rs b/cosmos-stdtx/src/schema/definition.rs new file mode 100644 index 0000000..4a98579 --- /dev/null +++ b/cosmos-stdtx/src/schema/definition.rs @@ -0,0 +1,40 @@ +//! Type definition within a schema + +use super::{field, Field, TypeName}; +use crate::error::{Error, ErrorKind}; +use anomaly::fail; +use serde::Deserialize; + +/// Definition of a particular type in the schema +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct Definition { + /// Name of the type this definition is for + type_name: TypeName, + + /// Fields in this type definition + #[serde(deserialize_with = "field::deserialize_vec")] + fields: Vec, +} + +impl Definition { + /// Create a new schema [`Definition`] with the given type name and fields + pub fn new(type_name: TypeName, fields: impl Into>) -> Result { + let fields = fields.into(); + + if let Err(e) = field::check_for_duplicate_tags(&fields) { + fail!(ErrorKind::Parse, "{}", e); + } + + Ok(Self { type_name, fields }) + } + + /// Get the [`TypeName`] defined by this schema. + pub fn type_name(&self) -> &TypeName { + &self.type_name + } + + /// Get a list of [`Field`] types in this schema. + pub fn fields(&self) -> &[Field] { + self.fields.as_slice() + } +} diff --git a/cosmos-stdtx/src/schema/field.rs b/cosmos-stdtx/src/schema/field.rs new file mode 100644 index 0000000..46a1f4c --- /dev/null +++ b/cosmos-stdtx/src/schema/field.rs @@ -0,0 +1,90 @@ +//! Fields in a type definition + +use super::ValueType; +use serde::{de, Deserialize}; +use std::collections::BTreeSet as Set; + +/// Fields in an Amino-serialized `sdk.Msg` +#[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +pub struct Field { + /// Field number to use as the key in an Amino message. + /// + /// These are all ensured to be `Some` in the `deserialize_vec` method below. + tag: Option, + + /// Amino type to serialize this field as + #[serde(rename = "type")] + value_type: ValueType, +} + +impl Field { + /// Create a new [`Field`] with the given tag and [`ValueType`] + pub fn new(tag: u64, value_type: ValueType) -> Self { + Self { + tag: Some(tag), + value_type, + } + } + + /// Get the numerical tag for this [`Field`] + pub fn tag(&self) -> u64 { + self.tag.unwrap() + } + + /// Get the [`ValueType`] for this [`Field`] + pub fn value_type(&self) -> ValueType { + self.value_type + } +} + +/// Deserialize `Vec`, populating their `tag` if unpopulated +pub(super) fn deserialize_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, +{ + let mut fields: Vec = Vec::deserialize(deserializer)?; + populate_tags(&mut fields).map_err(de::Error::custom)?; + check_for_duplicate_tags(&fields).map_err(de::Error::custom)?; + Ok(fields) +} + +/// Populate the `tag` for [`Field`] values if unset +fn populate_tags(fields: &mut [Field]) -> Result<(), &str> { + // Tags are 1-indexed + let mut tag = 1; + + for field in fields { + match field.tag { + Some(t) => { + if t == 0 { + // `0` is not allowed as a field tag + return Err("invalid field tag: 0"); + } + + // auto index by last specified tag + tag = t + 1 + } + None => { + field.tag = Some(tag); + tag += 1; + } + } + } + + Ok(()) +} + +/// Ensure tags are unique across all fields +pub(super) fn check_for_duplicate_tags(fields: &[Field]) -> Result<(), String> { + let mut tags = Set::new(); + + for field in fields { + let tag = field.tag.unwrap(); + + if !tags.insert(tag) { + return Err(format!("duplicate field tag: {}", tag)); + } + } + + Ok(()) +} diff --git a/cosmos-stdtx/src/schema/type_name.rs b/cosmos-stdtx/src/schema/type_name.rs new file mode 100644 index 0000000..ca0c800 --- /dev/null +++ b/cosmos-stdtx/src/schema/type_name.rs @@ -0,0 +1,78 @@ +//! Amino type names + +use crate::error::{Error, ErrorKind}; +use anomaly::fail; +use serde::{de, Deserialize}; +use sha2::{Digest, Sha256}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +/// Name of an Amino type +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +pub struct TypeName(String); + +impl TypeName { + /// Create a new `sdk.Msg` type name + pub fn new(name: impl AsRef) -> Result { + name.as_ref().parse() + } + + /// Borrow this [`TypeName`] as a string + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Compute the Amino prefix for this [`TypeName`] + pub fn amino_prefix(&self) -> Vec { + Sha256::digest(self.0.as_bytes()) + .iter() + .filter(|&x| *x != 0x00) + .skip(3) + .filter(|&x| *x != 0x00) + .cloned() + .take(4) + .collect() + } +} + +impl AsRef for TypeName { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for TypeName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl<'de> Deserialize<'de> for TypeName { + fn deserialize>(deserializer: D) -> Result { + use de::Error; + let s = String::deserialize(deserializer)?; + s.parse().map_err(D::Error::custom) + } +} + +impl FromStr for TypeName { + type Err = Error; + + fn from_str(s: &str) -> Result { + for c in s.chars() { + match c { + 'A'..='Z' | 'a'..='z' | '0'..='9' | '/' => (), + _ => fail!( + ErrorKind::Parse, + "invalid character `{}` in type name: `{}`", + c, + s + ), + } + } + + Ok(TypeName(s.to_owned())) + } +} diff --git a/cosmos-stdtx/src/schema/value_type.rs b/cosmos-stdtx/src/schema/value_type.rs new file mode 100644 index 0000000..67cd636 --- /dev/null +++ b/cosmos-stdtx/src/schema/value_type.rs @@ -0,0 +1,63 @@ +//! Types of values that can be present in an `sdk.Msg` + +use crate::error::{Error, ErrorKind}; +use anomaly::fail; +use serde::{de, Deserialize}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; + +/// Types of Amino values which can be included in a [`sdk.Msg`] +/// +/// [`sdk.Msg`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ValueType { + /// `sdk.AccAddress`: Cosmos SDK account addresses + /// + SdkAccAddress, + + /// `sdk.Dec`: Cosmos SDK decimals + /// + SdkDecimal, + + /// `sdk.ValAddress`: Cosmos SDK validator addresses + /// + SdkValAddress, + + /// Strings + String, +} + +impl Display for ValueType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + ValueType::SdkAccAddress => "sdk.AccAddress", + ValueType::SdkDecimal => "sdk.Dec", + ValueType::SdkValAddress => "sdk.ValAddress", + ValueType::String => "string", + }) + } +} + +impl FromStr for ValueType { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "sdk.AccAddress" => ValueType::SdkAccAddress, + "sdk.Dec" => ValueType::SdkDecimal, + "sdk.ValAddress" => ValueType::SdkValAddress, + "string" => ValueType::String, + _ => fail!(ErrorKind::Parse, "unknown value type: `{}`", s), + }) + } +} + +impl<'de> Deserialize<'de> for ValueType { + fn deserialize>(deserializer: D) -> Result { + use de::Error; + let s = String::deserialize(deserializer)?; + s.parse().map_err(D::Error::custom) + } +} diff --git a/cosmos-stdtx/tests/integration.rs b/cosmos-stdtx/tests/integration.rs new file mode 100644 index 0000000..6dd12b2 --- /dev/null +++ b/cosmos-stdtx/tests/integration.rs @@ -0,0 +1,19 @@ +//! `cosmos-stdtx` integration tests + +use cosmos_stdtx::Schema; + +/// Path to an example schema TOML file +const EXAMPLE_SCHEMA: &str = "tests/support/example_schema.toml"; + +/// Load an example [`Schema`] from a TOML file +#[test] +fn load_schema() { + let schema = Schema::load_toml(EXAMPLE_SCHEMA).unwrap(); + assert_eq!(schema.definitions().len(), 2); + + for definition in schema.definitions() { + for (i, field) in definition.fields().iter().enumerate() { + assert_eq!(i + 1, field.tag() as usize); + } + } +} diff --git a/cosmos-stdtx/tests/support/example_schema.toml b/cosmos-stdtx/tests/support/example_schema.toml new file mode 100644 index 0000000..5a5c61f --- /dev/null +++ b/cosmos-stdtx/tests/support/example_schema.toml @@ -0,0 +1,28 @@ +# Example StdTx message schema definition. +# +# Message types taken from Terra's oracle voter transactions: +# + +# StdTx namespace for schema definitions +# (e.g. `cosmos-sdk/StdTx` for Cosmos SDK) +namespace = "core/StdTx" + +[[definition]] +type_name = "oracle/MsgExchangeRatePrevote" +fields = [ + { name = "hash", type = "string" }, + { name = "denom", type = "string" }, + { name = "feeder", type = "sdk.AccAddress" }, + { name = "validator", type = "sdk.ValAddress" }, +] + +[[definition]] +type_name = "oracle/MsgExchangeRateVote" +fields = [ + # explicit field tag example - will start from "1" otherwise + { name = "exchange_rate", type = "sdk.Dec", tag = 1 }, + { name = "salt", type = "string" }, + { name = "denom", type = "string" }, + { name = "feeder", type = "sdk.AccAddress" }, + { name = "validator", type = "sdk.ValAddress" }, +] From 22c11d7f2f7e79d80019229e885d3c5649519905 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 26 Jan 2020 09:03:59 -0800 Subject: [PATCH 2/5] cosmos-stdtx: Msg builder and encoder Support for programatically building messages from fields and values, validating them against the schema, and encoding the resulting messages as Amino. --- Cargo.lock | 73 +++++++++ cosmos-stdtx/Cargo.toml | 3 + cosmos-stdtx/README.md | 6 + cosmos-stdtx/src/address.rs | 48 ++++++ cosmos-stdtx/src/error.rs | 14 ++ cosmos-stdtx/src/lib.rs | 5 +- cosmos-stdtx/src/msg.rs | 49 ++++++ cosmos-stdtx/src/msg/builder.rs | 176 +++++++++++++++++++++ cosmos-stdtx/src/msg/value.rs | 57 +++++++ cosmos-stdtx/src/schema.rs | 44 +++++- cosmos-stdtx/src/schema/definition.rs | 44 +++++- cosmos-stdtx/src/schema/field.rs | 29 ++-- cosmos-stdtx/src/{schema => }/type_name.rs | 2 +- 13 files changed, 532 insertions(+), 18 deletions(-) create mode 100644 cosmos-stdtx/src/address.rs create mode 100644 cosmos-stdtx/src/msg.rs create mode 100644 cosmos-stdtx/src/msg/builder.rs create mode 100644 cosmos-stdtx/src/msg/value.rs rename cosmos-stdtx/src/{schema => }/type_name.rs (96%) diff --git a/Cargo.lock b/Cargo.lock index 6d62a8e..bb5e613 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,8 +387,11 @@ name = "cosmos-stdtx" version = "0.0.1" dependencies = [ "anomaly", + "prost-amino", + "rust_decimal", "serde", "sha2", + "subtle-encoding 0.5.0", "thiserror", "toml", ] @@ -1125,6 +1128,41 @@ dependencies = [ "version_check", ] +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f115de20ad793e857f76da2563ff4a09fbcfd6fe93cca0c5d996ab5f3ee38d" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg 1.0.0", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.42" @@ -1135,6 +1173,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4dc79f9e6c81bef96148c8f6b8e72ad4541caa4a24373e900a36da07de03a3" +dependencies = [ + "autocfg 1.0.0", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.11" @@ -1573,6 +1634,18 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rust_decimal" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e33d8f7b289776cbd63687e26f5f25ca4b624c8eb1c9d5ea7c2cae2097e7fb" +dependencies = [ + "byteorder", + "bytes", + "num", + "serde", +] + [[package]] name = "rustc-demangle" version = "0.1.16" diff --git a/cosmos-stdtx/Cargo.toml b/cosmos-stdtx/Cargo.toml index d2ffb8c..0c226dc 100644 --- a/cosmos-stdtx/Cargo.toml +++ b/cosmos-stdtx/Cargo.toml @@ -15,7 +15,10 @@ circle-ci = { repository = "tendermint/kms" } [dependencies] anomaly = "0.1" +prost-amino = "0.5" +rust_decimal = "1.1" serde = { version = "1", features = ["serde_derive"] } sha2 = "0.8" +subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" toml = "0.5" diff --git a/cosmos-stdtx/README.md b/cosmos-stdtx/README.md index 6832092..2e9556f 100644 --- a/cosmos-stdtx/README.md +++ b/cosmos-stdtx/README.md @@ -1,7 +1,9 @@ # cosmos-stdtx.rs 🌌 [![Crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] [![Build Status][build-image]][build-link] +[![Safety Dance][safety-image]][safety-link] [![Apache 2.0 Licensed][license-image]][license-link] ![MSRV][rustc-image] @@ -44,8 +46,12 @@ limitations under the License. [crate-image]: https://img.shields.io/crates/v/cosmos-stdtx.svg [crate-link]: https://crates.io/crates/cosmos-stdtx +[docs-image]: https://docs.rs/cosmos-stdtx/badge.svg +[docs-link]: https://docs.rs/cosmos-stdtx/ [build-image]: https://circleci.com/gh/tendermint/kms.svg?style=shield [build-link]: https://circleci.com/gh/tendermint/kms +[safety-image]: https://img.shields.io/badge/unsafe-forbidden-success.svg +[safety-link]: https://github.com/rust-secure-code/safety-dance/ [license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg [license-link]: https://github.com/tendermint/kms/blob/master/LICENSE [rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg diff --git a/cosmos-stdtx/src/address.rs b/cosmos-stdtx/src/address.rs new file mode 100644 index 0000000..c537228 --- /dev/null +++ b/cosmos-stdtx/src/address.rs @@ -0,0 +1,48 @@ +//! Address types (account or validator) + +use crate::error::{Error, ErrorKind}; +use anomaly::ensure; +use std::convert::TryInto; +use subtle_encoding::bech32; + +/// Size of an address +pub const ADDRESS_SIZE: usize = 20; + +/// Address type +#[derive(Clone, Debug)] +pub struct Address(pub [u8; ADDRESS_SIZE]); + +impl Address { + /// Parse an address from its Bech32 form + pub fn from_bech32(addr_bech32: impl AsRef) -> Result<(String, Address), Error> { + let (hrp, addr) = bech32::decode(addr_bech32.as_ref())?; + + ensure!( + addr.len() == ADDRESS_SIZE, + ErrorKind::Address, + "invalid length for decoded address: {} (expected {})", + addr.len(), + ADDRESS_SIZE + ); + + Ok((hrp, Address(addr.as_slice().try_into().unwrap()))) + } +} + +impl AsRef<[u8]> for Address { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl From<[u8; ADDRESS_SIZE]> for Address { + fn from(addr: [u8; ADDRESS_SIZE]) -> Address { + Address(addr) + } +} + +impl From
for [u8; ADDRESS_SIZE] { + fn from(addr: Address) -> [u8; ADDRESS_SIZE] { + addr.0 + } +} diff --git a/cosmos-stdtx/src/error.rs b/cosmos-stdtx/src/error.rs index 7cdb879..36c1e48 100644 --- a/cosmos-stdtx/src/error.rs +++ b/cosmos-stdtx/src/error.rs @@ -10,6 +10,10 @@ use thiserror::Error; /// Kinds of errors #[derive(Copy, Clone, Debug, Error, Eq, PartialEq)] pub enum ErrorKind { + /// Malformed account or validator address + #[error("address error")] + Address, + /// Input/output errors #[error("I/O error")] Io, @@ -17,6 +21,10 @@ pub enum ErrorKind { /// Parse error #[error("parse error")] Parse, + + /// Invalid type + #[error("type error")] + Type, } impl ErrorKind { @@ -62,6 +70,12 @@ impl From> for Error { } } +impl From for Error { + fn from(source: subtle_encoding::Error) -> Error { + Context::new(ErrorKind::Parse, Some(source.into())).into() + } +} + impl From for Error { fn from(source: toml::de::Error) -> Error { Context::new(ErrorKind::Parse, Some(source.into())).into() diff --git a/cosmos-stdtx/src/lib.rs b/cosmos-stdtx/src/lib.rs index a7f7cc9..11434d9 100644 --- a/cosmos-stdtx/src/lib.rs +++ b/cosmos-stdtx/src/lib.rs @@ -12,7 +12,10 @@ #![forbid(unsafe_code)] #![warn(rust_2018_idioms, missing_docs, unused_qualifications)] +pub mod address; pub mod error; +pub mod msg; pub mod schema; +pub mod type_name; -pub use self::{error::Error, schema::Schema}; +pub use self::{address::Address, error::Error, msg::Msg, schema::Schema, type_name::TypeName}; diff --git a/cosmos-stdtx/src/msg.rs b/cosmos-stdtx/src/msg.rs new file mode 100644 index 0000000..38b2e3e --- /dev/null +++ b/cosmos-stdtx/src/msg.rs @@ -0,0 +1,49 @@ +//! Transaction message type (i.e `sdk.Msg`) + +mod builder; +mod value; + +pub use self::{builder::Builder, value::Value}; +pub use rust_decimal::Decimal; + +use crate::type_name::TypeName; +use prost_amino::encode_length_delimiter as encode_leb128; // Little-endian Base 128 + +/// Tags are indexes which identify message fields +pub type Tag = u64; + +/// Fields in the message +pub type Field = (Tag, Value); + +/// Transaction message type (i.e. [`sdk.Msg`]). +/// These serve as the payload for [`StdTx`] transactions. +/// +/// [`StdTx`]: https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx +/// [`sdk.Msg`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg +#[derive(Clone, Debug)] +pub struct Msg { + /// Name of the message type + type_name: TypeName, + + /// Fields in the message + fields: Vec, +} + +impl Msg { + /// Encode this message in the Amino wire format + pub fn to_amino_bytes(&self) -> Vec { + let mut result = self.type_name.amino_prefix(); + + for (tag, value) in &self.fields { + // Compute the field prefix, which encodes the tag and wire type code + let prefix = *tag << 3 | value.wire_type(); + encode_leb128(prefix as usize, &mut result).expect("LEB128 encoding error"); + + let mut encoded_value = value.to_amino_bytes(); + encode_leb128(encoded_value.len(), &mut result).expect("LEB128 encoding error"); + result.append(&mut encoded_value); + } + + result + } +} diff --git a/cosmos-stdtx/src/msg/builder.rs b/cosmos-stdtx/src/msg/builder.rs new file mode 100644 index 0000000..5770462 --- /dev/null +++ b/cosmos-stdtx/src/msg/builder.rs @@ -0,0 +1,176 @@ +//! Transaction message builder + +use super::{Field, Msg, Value}; +use crate::{ + address::Address, + error::{Error, ErrorKind}, + schema::{Definition, Schema, ValueType}, + type_name::TypeName, +}; +use anomaly::{ensure, format_err}; +use rust_decimal::Decimal; +use std::convert::TryInto; + +/// Transaction message builder +pub struct Builder<'a> { + /// Schema for the message we're building + schema_definition: &'a Definition, + + /// Name of the message type + type_name: TypeName, + + /// Bech32 prefix for account addresses + acc_address_prefix: Option, + + /// Bech32 prefix for validator consensus addresses + val_address_prefix: Option, + + /// Fields in the message + fields: Vec, +} + +impl<'a> Builder<'a> { + /// Create a new message builder for the given schema and message type + pub fn new( + schema: &'a Schema, + type_name: impl TryInto, + ) -> Result { + let type_name = type_name.try_into()?; + + let schema_definition = schema.get_definition(&type_name).ok_or_else(|| { + format_err!( + ErrorKind::Type, + "type not found in schema: `{}`", + &type_name + ) + })?; + + let acc_address_prefix = schema.acc_address_prefix().map(ToString::to_string); + let val_address_prefix = schema.val_address_prefix().map(ToString::to_string); + + Ok(Self { + schema_definition, + type_name, + acc_address_prefix, + val_address_prefix, + fields: vec![], + }) + } + + /// `sdk.AccAddress`: Cosmos SDK account addresses + /// + pub fn acc_address( + &mut self, + field_name: &TypeName, + address: Address, + ) -> Result<&mut Self, Error> { + let tag = self + .schema_definition + .get_field_tag(field_name, ValueType::SdkAccAddress)?; + + let field = (tag, Value::SdkAccAddress(address)); + + self.fields.push(field); + Ok(self) + } + + /// `sdk.AccAddress` encoded as Bech32 + pub fn acc_address_bech32( + &mut self, + field_name: &TypeName, + addr_bech32: impl AsRef, + ) -> Result<&mut Self, Error> { + let (hrp, address) = Address::from_bech32(addr_bech32)?; + + if let Some(prefix) = &self.acc_address_prefix { + ensure!( + &hrp == prefix, + ErrorKind::Address, + "invalid account address prefix: `{}` (expected `{}`)", + hrp, + prefix, + ); + } + + self.acc_address(field_name, address) + } + + /// `sdk.Dec`: Cosmos SDK decimals + /// s + pub fn decimal( + &mut self, + field_name: &TypeName, + value: impl Into, + ) -> Result<&mut Self, Error> { + let tag = self + .schema_definition + .get_field_tag(field_name, ValueType::SdkDecimal)?; + + let field = (tag, Value::SdkDecimal(value.into())); + + self.fields.push(field); + Ok(self) + } + + /// `sdk.ValAddress`: Cosmos SDK validator addresses + /// + pub fn val_address( + &mut self, + field_name: &TypeName, + address: Address, + ) -> Result<&mut Self, Error> { + let tag = self + .schema_definition + .get_field_tag(field_name, ValueType::SdkValAddress)?; + + let field = (tag, Value::SdkValAddress(address)); + + self.fields.push(field); + Ok(self) + } + + /// `sdk.ValAddress` encoded as Bech32 + pub fn val_address_bech32( + &mut self, + field_name: &TypeName, + addr_bech32: impl AsRef, + ) -> Result<&mut Self, Error> { + let (hrp, address) = Address::from_bech32(addr_bech32)?; + + if let Some(prefix) = &self.val_address_prefix { + ensure!( + &hrp == prefix, + ErrorKind::Address, + "invalid validator address prefix: `{}` (expected `{}`)", + hrp, + prefix, + ); + } + + self.val_address(field_name, address) + } + + /// Strings + pub fn string( + &mut self, + field_name: &TypeName, + s: impl Into, + ) -> Result<&mut Self, Error> { + let tag = self + .schema_definition + .get_field_tag(field_name, ValueType::String)?; + + let field = (tag, Value::String(s.into())); + + self.fields.push(field); + Ok(self) + } + + /// Consume this builder and output a message + pub fn into_msg(self) -> Msg { + Msg { + type_name: self.type_name, + fields: self.fields, + } + } +} diff --git a/cosmos-stdtx/src/msg/value.rs b/cosmos-stdtx/src/msg/value.rs new file mode 100644 index 0000000..c197804 --- /dev/null +++ b/cosmos-stdtx/src/msg/value.rs @@ -0,0 +1,57 @@ +//! Message values + +use crate::{address::Address, schema::ValueType}; +use rust_decimal::Decimal; + +/// Message values - data contained in fields of a message +#[derive(Clone, Debug)] +pub enum Value { + /// `sdk.AccAddress`: Cosmos SDK account addresses + /// + SdkAccAddress(Address), + + /// `sdk.Dec`: Cosmos SDK decimals + /// + SdkDecimal(Decimal), + + /// `sdk.ValAddress`: Cosmos SDK validator addresses + /// + SdkValAddress(Address), + + /// Strings + String(String), +} + +impl Value { + /// Get the type of this value + pub fn value_type(&self) -> ValueType { + match self { + Value::SdkAccAddress(_) => ValueType::SdkAccAddress, + Value::SdkDecimal(_) => ValueType::SdkDecimal, + Value::SdkValAddress(_) => ValueType::SdkValAddress, + Value::String(_) => ValueType::String, + } + } + + /// Get the Amino/Proto wire type for this field + /// See: + pub(super) fn wire_type(&self) -> u64 { + match self { + // Length-delimited types + Value::SdkAccAddress(_) + | Value::SdkDecimal(_) + | Value::SdkValAddress(_) + | Value::String(_) => 2, + } + } + + /// Encode this value as Amino bytes + pub(super) fn to_amino_bytes(&self) -> Vec { + match self { + Value::SdkAccAddress(addr) | Value::SdkValAddress(addr) => addr.as_ref().to_vec(), + // TODO(tarcieri): check that decimals are being encoded correctly + Value::SdkDecimal(decimal) => decimal.to_string().as_bytes().to_vec(), + Value::String(s) => s.as_bytes().to_vec(), + } + } +} diff --git a/cosmos-stdtx/src/schema.rs b/cosmos-stdtx/src/schema.rs index fce2bad..1b7d9bb 100644 --- a/cosmos-stdtx/src/schema.rs +++ b/cosmos-stdtx/src/schema.rs @@ -20,12 +20,14 @@ mod definition; mod field; -mod type_name; mod value_type; -pub use self::{definition::Definition, field::Field, type_name::TypeName, value_type::ValueType}; +pub use self::{definition::Definition, field::Field, value_type::ValueType}; -use crate::error::{Error, ErrorKind}; +use crate::{ + error::{Error, ErrorKind}, + type_name::TypeName, +}; use anomaly::fail; use serde::Deserialize; use std::{fs, path::Path, str::FromStr}; @@ -37,10 +39,17 @@ use std::{fs, path::Path, str::FromStr}; /// [`StdTx`]: https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx /// [`sdk.Msg`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] pub struct Schema { /// `StdTx` namespace for schema namespace: TypeName, + /// Bech32 prefix for account addresses + acc_address_prefix: Option, + + /// Bech32 prefix for validator consensus addresses + val_address_prefix: Option, + /// Schema definitions #[serde(rename = "definition")] definitions: Vec, @@ -48,9 +57,19 @@ pub struct Schema { impl Schema { /// Create a new [`Schema`] with the given `StdTx` namespace and [`Definition`] set - pub fn new(namespace: TypeName, definitions: impl Into>) -> Self { + pub fn new( + namespace: TypeName, + acc_address_prefix: Option>, + val_address_prefix: Option>, + definitions: impl Into>, + ) -> Self { + let acc_address_prefix = acc_address_prefix.as_ref().map(|s| s.as_ref().to_owned()); + let val_address_prefix = val_address_prefix.as_ref().map(|s| s.as_ref().to_owned()); + Self { namespace, + acc_address_prefix, + val_address_prefix, definitions: definitions.into(), } } @@ -68,10 +87,27 @@ impl Schema { } } + /// Get the Bech32 prefix for account addresses + pub fn acc_address_prefix(&self) -> Option<&str> { + self.acc_address_prefix.as_ref().map(AsRef::as_ref) + } + + /// Get the Bech32 prefix for validator addresses + pub fn val_address_prefix(&self) -> Option<&str> { + self.val_address_prefix.as_ref().map(AsRef::as_ref) + } + /// [`Definition`] types found in this [`Schema`] pub fn definitions(&self) -> &[Definition] { &self.definitions } + + /// Get a schema [`Definition`] for the given [`TypeName`] + pub fn get_definition(&self, type_name: &TypeName) -> Option<&Definition> { + self.definitions + .iter() + .find(|def| def.type_name() == type_name) + } } impl FromStr for Schema { diff --git a/cosmos-stdtx/src/schema/definition.rs b/cosmos-stdtx/src/schema/definition.rs index 4a98579..30ecdaa 100644 --- a/cosmos-stdtx/src/schema/definition.rs +++ b/cosmos-stdtx/src/schema/definition.rs @@ -1,12 +1,16 @@ //! Type definition within a schema -use super::{field, Field, TypeName}; -use crate::error::{Error, ErrorKind}; -use anomaly::fail; +use super::{field, Field, TypeName, ValueType}; +use crate::{ + error::{Error, ErrorKind}, + msg::Tag, +}; +use anomaly::{fail, format_err}; use serde::Deserialize; /// Definition of a particular type in the schema #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] pub struct Definition { /// Name of the type this definition is for type_name: TypeName, @@ -37,4 +41,38 @@ impl Definition { pub fn fields(&self) -> &[Field] { self.fields.as_slice() } + + /// Get a [`Field`] by its [`TypeName`] + pub fn get_field(&self, field_name: &TypeName) -> Option<&Field> { + self.fields.iter().find(|field| field.name() == field_name) + } + + /// Get the [`Tag`] for a [`Field`], ensuring is of the given [`ValueType`] + pub fn get_field_tag( + &self, + field_name: &TypeName, + value_type: ValueType, + ) -> Result { + let field = self.get_field(field_name).ok_or_else(|| { + format_err!( + ErrorKind::Type, + "field name not found in `{}` schema: `{}`", + &self.type_name, + field_name + ) + })?; + + if field.value_type() != value_type { + fail!( + ErrorKind::Type, + "field `{}` of `{}` is not an {} (expected {})", + field_name, + &self.type_name, + value_type, + field.value_type() + ); + } + + Ok(field.tag()) + } } diff --git a/cosmos-stdtx/src/schema/field.rs b/cosmos-stdtx/src/schema/field.rs index 46a1f4c..eea96e3 100644 --- a/cosmos-stdtx/src/schema/field.rs +++ b/cosmos-stdtx/src/schema/field.rs @@ -1,40 +1,51 @@ //! Fields in a type definition use super::ValueType; +use crate::{msg::Tag, type_name::TypeName}; use serde::{de, Deserialize}; use std::collections::BTreeSet as Set; /// Fields in an Amino-serialized `sdk.Msg` #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] +#[serde(deny_unknown_fields)] pub struct Field { - /// Field number to use as the key in an Amino message. - /// - /// These are all ensured to be `Some` in the `deserialize_vec` method below. - tag: Option, + /// Name of this field + name: TypeName, /// Amino type to serialize this field as #[serde(rename = "type")] value_type: ValueType, + + /// Field number to use as the key in an Amino message. + /// + /// These are all ensured to be `Some` in the `deserialize_vec` method below. + tag: Option, } impl Field { /// Create a new [`Field`] with the given tag and [`ValueType`] - pub fn new(tag: u64, value_type: ValueType) -> Self { + pub fn new(name: TypeName, value_type: ValueType, tag: Tag) -> Self { Self { + name, tag: Some(tag), value_type, } } - /// Get the numerical tag for this [`Field`] - pub fn tag(&self) -> u64 { - self.tag.unwrap() + /// Get the [`TypeName`] for this [`Field`] + pub fn name(&self) -> &TypeName { + &self.name } /// Get the [`ValueType`] for this [`Field`] pub fn value_type(&self) -> ValueType { self.value_type } + + /// Get the numerical index [`Tag`] for this [`Field`] + pub fn tag(&self) -> Tag { + self.tag.unwrap() + } } /// Deserialize `Vec`, populating their `tag` if unpopulated @@ -79,7 +90,7 @@ pub(super) fn check_for_duplicate_tags(fields: &[Field]) -> Result<(), String> { let mut tags = Set::new(); for field in fields { - let tag = field.tag.unwrap(); + let tag = field.tag.expect("field with unpopulated tag!"); if !tags.insert(tag) { return Err(format!("duplicate field tag: {}", tag)); diff --git a/cosmos-stdtx/src/schema/type_name.rs b/cosmos-stdtx/src/type_name.rs similarity index 96% rename from cosmos-stdtx/src/schema/type_name.rs rename to cosmos-stdtx/src/type_name.rs index ca0c800..eab5b8d 100644 --- a/cosmos-stdtx/src/schema/type_name.rs +++ b/cosmos-stdtx/src/type_name.rs @@ -63,7 +63,7 @@ impl FromStr for TypeName { fn from_str(s: &str) -> Result { for c in s.chars() { match c { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '/' => (), + 'A'..='Z' | 'a'..='z' | '0'..='9' | '/' | '_' => (), _ => fail!( ErrorKind::Parse, "invalid character `{}` in type name: `{}`", From 1dc247f26dc384af05fa196a4786e2fcabe43afb Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 26 Jan 2020 09:17:09 -0800 Subject: [PATCH 3/5] cosmos-stdtx: add Amino types for encoding StdTx The schema-based encoder is only for `Msg`. This adds a `StdTx` type using `prost-amino-derive` to programatically declare it. --- Cargo.lock | 1 + cosmos-stdtx/Cargo.toml | 1 + cosmos-stdtx/src/lib.rs | 1 + cosmos-stdtx/src/stdtx.rs | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 cosmos-stdtx/src/stdtx.rs diff --git a/Cargo.lock b/Cargo.lock index bb5e613..f3aae68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -388,6 +388,7 @@ version = "0.0.1" dependencies = [ "anomaly", "prost-amino", + "prost-amino-derive", "rust_decimal", "serde", "sha2", diff --git a/cosmos-stdtx/Cargo.toml b/cosmos-stdtx/Cargo.toml index 0c226dc..ffa77c9 100644 --- a/cosmos-stdtx/Cargo.toml +++ b/cosmos-stdtx/Cargo.toml @@ -16,6 +16,7 @@ circle-ci = { repository = "tendermint/kms" } [dependencies] anomaly = "0.1" prost-amino = "0.5" +prost-amino-derive = "0.5" rust_decimal = "1.1" serde = { version = "1", features = ["serde_derive"] } sha2 = "0.8" diff --git a/cosmos-stdtx/src/lib.rs b/cosmos-stdtx/src/lib.rs index 11434d9..bc1d49f 100644 --- a/cosmos-stdtx/src/lib.rs +++ b/cosmos-stdtx/src/lib.rs @@ -16,6 +16,7 @@ pub mod address; pub mod error; pub mod msg; pub mod schema; +pub mod stdtx; pub mod type_name; pub use self::{address::Address, error::Error, msg::Msg, schema::Schema, type_name::TypeName}; diff --git a/cosmos-stdtx/src/stdtx.rs b/cosmos-stdtx/src/stdtx.rs new file mode 100644 index 0000000..c7f7252 --- /dev/null +++ b/cosmos-stdtx/src/stdtx.rs @@ -0,0 +1,75 @@ +//! StdTx Amino types + +use crate::type_name::TypeName; +use prost_amino::{encode_length_delimiter, Message}; +use prost_amino_derive::Message; + +/// StdTx Amino type +#[derive(Clone, Message)] +pub struct StdTx { + /// Messages in transction + #[prost_amino(bytes, repeated, tag = "1")] + pub msg: Vec>, + + /// Feeds to be paid + #[prost_amino(message)] + pub fee: Option, + + /// Signatures + #[prost_amino(message, repeated)] + pub signatures: Vec, + + /// Memo field + #[prost_amino(string)] + pub memo: String, +} + +impl StdTx { + /// Encode this [`StdTx`] in Amino encoding identifying it with the given + /// type name (e.g. `cosmos-sdk/StdTx`) + pub fn to_amino_bytes(&self, type_name: &TypeName) -> Vec { + let mut amino_tx = type_name.amino_prefix(); + self.encode(&mut amino_tx).expect("LEB128 encoding error"); + + let mut amino_encoded = vec![]; + encode_length_delimiter(amino_tx.len(), &mut amino_encoded).expect("LEB128 encoding error"); + amino_encoded.append(&mut amino_tx); + amino_encoded + } +} + +/// StdFee amino type +#[derive(Clone, Message)] +pub struct StdFee { + /// Fee to be paid + #[prost_amino(message, repeated, tag = "1")] + pub amount: Vec, + + /// Gas requested for transaction + #[prost_amino(uint64)] + pub gas: u64, +} + +/// Coin amino type +#[derive(Clone, Message)] +pub struct Coin { + /// Denomination of coin + #[prost_amino(string, tag = "1")] + pub denom: String, + + /// Amount of the given denomination + #[prost_amino(string)] + pub amount: String, +} + +/// StdSignature amino type +#[derive(Clone, Message)] +pub struct StdSignature { + /// Public key which can verify this signature + #[prost_amino(bytes, tag = "1", amino_name = "tendermint/PubKeySecp256k1")] + pub pub_key: Vec, + + /// Serialized signature + #[prost_amino(bytes)] + pub signature: Vec, +} From be27fdf1d68d5f5c98099fd609c1bd110646a836 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Mon, 27 Jan 2020 06:19:28 -0800 Subject: [PATCH 4/5] cosmos-stdtx: JSON serializers for computing/signing StdSignMsg --- .circleci/config.yml | 7 +- Cargo.lock | 2 + cosmos-stdtx/Cargo.toml | 2 + cosmos-stdtx/src/address.rs | 5 ++ cosmos-stdtx/src/msg.rs | 39 ++++++-- cosmos-stdtx/src/msg/builder.rs | 57 ++++++------ cosmos-stdtx/src/msg/field.rs | 43 +++++++++ cosmos-stdtx/src/msg/value.rs | 28 +++++- cosmos-stdtx/src/schema.rs | 23 +++-- cosmos-stdtx/src/schema/definition.rs | 2 +- cosmos-stdtx/src/schema/field.rs | 14 ++- cosmos-stdtx/src/stdtx.rs | 33 ++++++- cosmos-stdtx/src/stdtx/builder.rs | 89 +++++++++++++++++++ .../tests/support/example_schema.toml | 4 + 14 files changed, 286 insertions(+), 62 deletions(-) create mode 100644 cosmos-stdtx/src/msg/field.rs create mode 100644 cosmos-stdtx/src/stdtx/builder.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index 61a4657..e539c54 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,13 @@ jobs: build: docker: - image: tendermint/kms:build-2019-06-05-v0 # bump cache keys when modifying this + environment: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: -D warnings steps: - checkout - restore_cache: - key: cache-2019-06-05-v0 # bump save_cache key below too + key: cache-2020-01-27-v0 # bump save_cache key below too - run: name: Install Rust 1.39.0 # TODO: update Rust in the upstream Docker image command: | @@ -61,7 +64,7 @@ jobs: cargo build --features=softsign TMKMS_BIN=./target/debug/tmkms sh tests/support/run-harness-tests.sh - save_cache: - key: cache-2019-06-05-v0 # bump restore_cache key above too + key: cache-2020-01-27-v0 # bump restore_cache key above too paths: - "~/.cargo" - "./target" diff --git a/Cargo.lock b/Cargo.lock index f3aae68..7cc8482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,10 +387,12 @@ name = "cosmos-stdtx" version = "0.0.1" dependencies = [ "anomaly", + "ecdsa", "prost-amino", "prost-amino-derive", "rust_decimal", "serde", + "serde_json", "sha2", "subtle-encoding 0.5.0", "thiserror", diff --git a/cosmos-stdtx/Cargo.toml b/cosmos-stdtx/Cargo.toml index ffa77c9..c5ef79c 100644 --- a/cosmos-stdtx/Cargo.toml +++ b/cosmos-stdtx/Cargo.toml @@ -15,10 +15,12 @@ circle-ci = { repository = "tendermint/kms" } [dependencies] anomaly = "0.1" +ecdsa = { version = "0.4", features = ["k256"] } prost-amino = "0.5" prost-amino-derive = "0.5" rust_decimal = "1.1" serde = { version = "1", features = ["serde_derive"] } +serde_json = "1" sha2 = "0.8" subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" diff --git a/cosmos-stdtx/src/address.rs b/cosmos-stdtx/src/address.rs index c537228..51f0a46 100644 --- a/cosmos-stdtx/src/address.rs +++ b/cosmos-stdtx/src/address.rs @@ -27,6 +27,11 @@ impl Address { Ok((hrp, Address(addr.as_slice().try_into().unwrap()))) } + + /// Encode this address as Bech32 + pub fn to_bech32(&self, hrp: &str) -> String { + bech32::encode(hrp, &self.0) + } } impl AsRef<[u8]> for Address { diff --git a/cosmos-stdtx/src/msg.rs b/cosmos-stdtx/src/msg.rs index 38b2e3e..c8a98a8 100644 --- a/cosmos-stdtx/src/msg.rs +++ b/cosmos-stdtx/src/msg.rs @@ -1,20 +1,19 @@ //! Transaction message type (i.e `sdk.Msg`) mod builder; +mod field; mod value; -pub use self::{builder::Builder, value::Value}; +pub use self::{builder::Builder, field::Field, value::Value}; pub use rust_decimal::Decimal; -use crate::type_name::TypeName; +use crate::{Schema, TypeName}; use prost_amino::encode_length_delimiter as encode_leb128; // Little-endian Base 128 +use std::{collections::BTreeMap, iter::FromIterator}; /// Tags are indexes which identify message fields pub type Tag = u64; -/// Fields in the message -pub type Field = (Tag, Value); - /// Transaction message type (i.e. [`sdk.Msg`]). /// These serve as the payload for [`StdTx`] transactions. /// @@ -30,16 +29,40 @@ pub struct Msg { } impl Msg { + /// Compute `serde_json::Value` representing a `sdk.Msg` + pub fn to_json_value(&self, schema: &Schema) -> serde_json::Value { + // `BTreeMap` ensures fields are ordered for Cosmos's Canonical JSON + let mut values = BTreeMap::new(); + + for field in &self.fields { + values.insert( + field.name().to_string(), + field.value().to_json_value(schema), + ); + } + + let mut json = serde_json::Map::new(); + json.insert( + "type".to_owned(), + serde_json::Value::String(self.type_name.to_string()), + ); + json.insert( + "value".to_owned(), + serde_json::Map::from_iter(values.into_iter()).into(), + ); + serde_json::Value::Object(json) + } + /// Encode this message in the Amino wire format pub fn to_amino_bytes(&self) -> Vec { let mut result = self.type_name.amino_prefix(); - for (tag, value) in &self.fields { + for field in &self.fields { // Compute the field prefix, which encodes the tag and wire type code - let prefix = *tag << 3 | value.wire_type(); + let prefix = field.tag() << 3 | field.value().wire_type(); encode_leb128(prefix as usize, &mut result).expect("LEB128 encoding error"); - let mut encoded_value = value.to_amino_bytes(); + let mut encoded_value = field.value().to_amino_bytes(); encode_leb128(encoded_value.len(), &mut result).expect("LEB128 encoding error"); result.append(&mut encoded_value); } diff --git a/cosmos-stdtx/src/msg/builder.rs b/cosmos-stdtx/src/msg/builder.rs index 5770462..38d5d47 100644 --- a/cosmos-stdtx/src/msg/builder.rs +++ b/cosmos-stdtx/src/msg/builder.rs @@ -20,10 +20,10 @@ pub struct Builder<'a> { type_name: TypeName, /// Bech32 prefix for account addresses - acc_address_prefix: Option, + acc_prefix: String, /// Bech32 prefix for validator consensus addresses - val_address_prefix: Option, + val_prefix: String, /// Fields in the message fields: Vec, @@ -45,14 +45,11 @@ impl<'a> Builder<'a> { ) })?; - let acc_address_prefix = schema.acc_address_prefix().map(ToString::to_string); - let val_address_prefix = schema.val_address_prefix().map(ToString::to_string); - Ok(Self { schema_definition, type_name, - acc_address_prefix, - val_address_prefix, + acc_prefix: schema.acc_prefix().to_owned(), + val_prefix: schema.val_prefix().to_owned(), fields: vec![], }) } @@ -68,7 +65,7 @@ impl<'a> Builder<'a> { .schema_definition .get_field_tag(field_name, ValueType::SdkAccAddress)?; - let field = (tag, Value::SdkAccAddress(address)); + let field = Field::new(tag, field_name.clone(), Value::SdkAccAddress(address)); self.fields.push(field); Ok(self) @@ -82,15 +79,13 @@ impl<'a> Builder<'a> { ) -> Result<&mut Self, Error> { let (hrp, address) = Address::from_bech32(addr_bech32)?; - if let Some(prefix) = &self.acc_address_prefix { - ensure!( - &hrp == prefix, - ErrorKind::Address, - "invalid account address prefix: `{}` (expected `{}`)", - hrp, - prefix, - ); - } + ensure!( + hrp == self.acc_prefix, + ErrorKind::Address, + "invalid account address prefix: `{}` (expected `{}`)", + hrp, + self.acc_prefix, + ); self.acc_address(field_name, address) } @@ -106,7 +101,7 @@ impl<'a> Builder<'a> { .schema_definition .get_field_tag(field_name, ValueType::SdkDecimal)?; - let field = (tag, Value::SdkDecimal(value.into())); + let field = Field::new(tag, field_name.clone(), Value::SdkDecimal(value.into())); self.fields.push(field); Ok(self) @@ -123,7 +118,7 @@ impl<'a> Builder<'a> { .schema_definition .get_field_tag(field_name, ValueType::SdkValAddress)?; - let field = (tag, Value::SdkValAddress(address)); + let field = Field::new(tag, field_name.clone(), Value::SdkValAddress(address)); self.fields.push(field); Ok(self) @@ -137,15 +132,13 @@ impl<'a> Builder<'a> { ) -> Result<&mut Self, Error> { let (hrp, address) = Address::from_bech32(addr_bech32)?; - if let Some(prefix) = &self.val_address_prefix { - ensure!( - &hrp == prefix, - ErrorKind::Address, - "invalid validator address prefix: `{}` (expected `{}`)", - hrp, - prefix, - ); - } + ensure!( + hrp == self.val_prefix, + ErrorKind::Address, + "invalid validator address prefix: `{}` (expected `{}`)", + hrp, + self.val_prefix, + ); self.val_address(field_name, address) } @@ -160,17 +153,17 @@ impl<'a> Builder<'a> { .schema_definition .get_field_tag(field_name, ValueType::String)?; - let field = (tag, Value::String(s.into())); + let field = Field::new(tag, field_name.clone(), Value::String(s.into())); self.fields.push(field); Ok(self) } /// Consume this builder and output a message - pub fn into_msg(self) -> Msg { + pub fn to_msg(&self) -> Msg { Msg { - type_name: self.type_name, - fields: self.fields, + type_name: self.type_name.clone(), + fields: self.fields.clone(), } } } diff --git a/cosmos-stdtx/src/msg/field.rs b/cosmos-stdtx/src/msg/field.rs new file mode 100644 index 0000000..8494056 --- /dev/null +++ b/cosmos-stdtx/src/msg/field.rs @@ -0,0 +1,43 @@ +//! Message fields + +use super::{Tag, Value}; +use crate::type_name::TypeName; + +/// Message fields +#[derive(Clone, Debug)] +pub struct Field { + /// Field number to use as the key in an Amino message. + tag: Tag, + + /// Name of this field + name: TypeName, + + /// Amino type to serialize this field as + value: Value, +} + +impl Field { + /// Create a new message field + pub fn new(tag: Tag, name: TypeName, value: impl Into) -> Self { + Self { + tag, + name, + value: value.into(), + } + } + + /// Get this field's [`Tag`] + pub fn tag(&self) -> Tag { + self.tag + } + + /// Get this field's [`TypeName`] + pub fn name(&self) -> &TypeName { + &self.name + } + + /// Get this field's [`Value`] + pub fn value(&self) -> &Value { + &self.value + } +} diff --git a/cosmos-stdtx/src/msg/value.rs b/cosmos-stdtx/src/msg/value.rs index c197804..3c0251c 100644 --- a/cosmos-stdtx/src/msg/value.rs +++ b/cosmos-stdtx/src/msg/value.rs @@ -1,6 +1,9 @@ //! Message values -use crate::{address::Address, schema::ValueType}; +use crate::{ + address::Address, + schema::{Schema, ValueType}, +}; use rust_decimal::Decimal; /// Message values - data contained in fields of a message @@ -54,4 +57,27 @@ impl Value { Value::String(s) => s.as_bytes().to_vec(), } } + + /// Encode this value as a [`serde_json::Value`] + pub(super) fn to_json_value(&self, schema: &Schema) -> serde_json::Value { + serde_json::Value::String(match self { + Value::SdkAccAddress(addr) => addr.to_bech32(schema.acc_prefix()), + // TODO(tarcieri): check that decimals are being encoded correctly + Value::SdkDecimal(decimal) => decimal.to_string(), + Value::SdkValAddress(addr) => addr.to_bech32(schema.val_prefix()), + Value::String(s) => s.clone(), + }) + } +} + +impl From for Value { + fn from(dec: Decimal) -> Value { + Value::SdkDecimal(dec) + } +} + +impl From for Value { + fn from(s: String) -> Value { + Value::String(s) + } } diff --git a/cosmos-stdtx/src/schema.rs b/cosmos-stdtx/src/schema.rs index 1b7d9bb..ebe0d4f 100644 --- a/cosmos-stdtx/src/schema.rs +++ b/cosmos-stdtx/src/schema.rs @@ -45,10 +45,10 @@ pub struct Schema { namespace: TypeName, /// Bech32 prefix for account addresses - acc_address_prefix: Option, + acc_prefix: String, /// Bech32 prefix for validator consensus addresses - val_address_prefix: Option, + val_prefix: String, /// Schema definitions #[serde(rename = "definition")] @@ -59,17 +59,14 @@ impl Schema { /// Create a new [`Schema`] with the given `StdTx` namespace and [`Definition`] set pub fn new( namespace: TypeName, - acc_address_prefix: Option>, - val_address_prefix: Option>, + acc_prefix: impl Into, + val_prefix: impl Into, definitions: impl Into>, ) -> Self { - let acc_address_prefix = acc_address_prefix.as_ref().map(|s| s.as_ref().to_owned()); - let val_address_prefix = val_address_prefix.as_ref().map(|s| s.as_ref().to_owned()); - Self { namespace, - acc_address_prefix, - val_address_prefix, + acc_prefix: acc_prefix.into(), + val_prefix: val_prefix.into(), definitions: definitions.into(), } } @@ -88,13 +85,13 @@ impl Schema { } /// Get the Bech32 prefix for account addresses - pub fn acc_address_prefix(&self) -> Option<&str> { - self.acc_address_prefix.as_ref().map(AsRef::as_ref) + pub fn acc_prefix(&self) -> &str { + self.acc_prefix.as_ref() } /// Get the Bech32 prefix for validator addresses - pub fn val_address_prefix(&self) -> Option<&str> { - self.val_address_prefix.as_ref().map(AsRef::as_ref) + pub fn val_prefix(&self) -> &str { + self.val_prefix.as_ref() } /// [`Definition`] types found in this [`Schema`] diff --git a/cosmos-stdtx/src/schema/definition.rs b/cosmos-stdtx/src/schema/definition.rs index 30ecdaa..9de3ee2 100644 --- a/cosmos-stdtx/src/schema/definition.rs +++ b/cosmos-stdtx/src/schema/definition.rs @@ -25,7 +25,7 @@ impl Definition { pub fn new(type_name: TypeName, fields: impl Into>) -> Result { let fields = fields.into(); - if let Err(e) = field::check_for_duplicate_tags(&fields) { + if let Err(e) = field::validate(&fields) { fail!(ErrorKind::Parse, "{}", e); } diff --git a/cosmos-stdtx/src/schema/field.rs b/cosmos-stdtx/src/schema/field.rs index eea96e3..8c066c6 100644 --- a/cosmos-stdtx/src/schema/field.rs +++ b/cosmos-stdtx/src/schema/field.rs @@ -23,7 +23,7 @@ pub struct Field { } impl Field { - /// Create a new [`Field`] with the given tag and [`ValueType`] + /// Create a new [`Field`] with the given tag and [`ValueType`]. pub fn new(name: TypeName, value_type: ValueType, tag: Tag) -> Self { Self { name, @@ -55,7 +55,7 @@ where { let mut fields: Vec = Vec::deserialize(deserializer)?; populate_tags(&mut fields).map_err(de::Error::custom)?; - check_for_duplicate_tags(&fields).map_err(de::Error::custom)?; + validate(&fields).map_err(de::Error::custom)?; Ok(fields) } @@ -85,13 +85,19 @@ fn populate_tags(fields: &mut [Field]) -> Result<(), &str> { Ok(()) } -/// Ensure tags are unique across all fields -pub(super) fn check_for_duplicate_tags(fields: &[Field]) -> Result<(), String> { +/// Ensure field names and tags are unique across all fields +pub(super) fn validate(fields: &[Field]) -> Result<(), String> { + let mut names = Set::new(); let mut tags = Set::new(); for field in fields { + // This invariant is enforced in `populate_tags` and the `Field::new` methods let tag = field.tag.expect("field with unpopulated tag!"); + if !names.insert(&field.name) { + return Err(format!("duplicate field name: `{}`", &field.name)); + } + if !tags.insert(tag) { return Err(format!("duplicate field tag: {}", tag)); } diff --git a/cosmos-stdtx/src/stdtx.rs b/cosmos-stdtx/src/stdtx.rs index c7f7252..1879d6e 100644 --- a/cosmos-stdtx/src/stdtx.rs +++ b/cosmos-stdtx/src/stdtx.rs @@ -1,8 +1,13 @@ //! StdTx Amino types +mod builder; + +pub use self::builder::Builder; + use crate::type_name::TypeName; use prost_amino::{encode_length_delimiter, Message}; use prost_amino_derive::Message; +use serde_json::json; /// StdTx Amino type #[derive(Clone, Message)] @@ -50,7 +55,23 @@ pub struct StdFee { pub gas: u64, } -/// Coin amino type +impl StdFee { + /// Compute `serde_json::Value` representing this fee + pub fn to_json_value(&self) -> serde_json::Value { + let amount = self + .amount + .iter() + .map(|amt| amt.to_json_value()) + .collect::>(); + + json!({ + "amount": amount, + "gas": self.gas.to_string() + }) + } +} + +/// Coin Amino type #[derive(Clone, Message)] pub struct Coin { /// Denomination of coin @@ -62,6 +83,16 @@ pub struct Coin { pub amount: String, } +impl Coin { + /// Compute `serde_json::Value` representing this coin + pub fn to_json_value(&self) -> serde_json::Value { + json!({ + "denom": self.denom, + "amount": self.amount + }) + } +} + /// StdSignature amino type #[derive(Clone, Message)] pub struct StdSignature { diff --git a/cosmos-stdtx/src/stdtx/builder.rs b/cosmos-stdtx/src/stdtx/builder.rs new file mode 100644 index 0000000..b02b1ef --- /dev/null +++ b/cosmos-stdtx/src/stdtx/builder.rs @@ -0,0 +1,89 @@ +//! Builder for `StdTx` transactions which handles construction and signing. + +pub use ecdsa::{curve::secp256k1::FixedSignature as Signature, signature::Signer as _}; + +use super::{StdFee, StdTx}; +use crate::{error::Error, msg::Msg, schema::Schema}; +use serde_json::json; + +/// Transaction signer +pub type Signer = dyn ecdsa::signature::Signer; + +/// [`StdTx`] transaction builder, which handles construction, signing, and +/// Amino serialization. +pub struct Builder { + /// Schema which describes valid transaction types + schema: Schema, + + /// Account number to include in transactions + account_number: u64, + + /// Chain ID + chain_id: String, + + /// Transaction signer + signer: Box, +} + +impl Builder { + /// Create a new transaction builder + pub fn new( + schema: Schema, + account_number: u64, + chain_id: impl Into, + signer: impl Into>, + ) -> Self { + Self { + schema, + account_number, + chain_id: chain_id.into(), + signer: signer.into(), + } + } + + /// Borrow this transaction builder's [`Schema`] + pub fn schema(&self) -> &Schema { + &self.schema + } + + /// Get this transaction builder's account number + pub fn account_number(&self) -> u64 { + self.account_number + } + + /// Borrow this transaction builder's chain ID + pub fn chain_id(&self) -> &str { + &self.chain_id + } + + /// Build and sign a transaction containing the given messages + pub fn sign_tx( + &self, + sequence: u64, + fee: &StdFee, + memo: &str, + messages: &[Msg], + ) -> Result { + let sign_msg = self.create_sign_msg(sequence, fee, memo, messages); + let _signature = self.signer.sign(sign_msg.as_bytes()); + unimplemented!(); + } + + /// Create the JSON message to sign for this transaction + fn create_sign_msg(&self, sequence: u64, fee: &StdFee, memo: &str, messages: &[Msg]) -> String { + let messages = messages + .iter() + .map(|msg| msg.to_json_value(&self.schema)) + .collect::>(); + + json!({ + "account_number": self.account_number, + "chain_id": self.chain_id, + "fee": fee.to_json_value(), + "memo": memo, + "msgs": messages, + "sequence": sequence.to_string() + }) + .to_string() + } +} diff --git a/cosmos-stdtx/tests/support/example_schema.toml b/cosmos-stdtx/tests/support/example_schema.toml index 5a5c61f..4293f10 100644 --- a/cosmos-stdtx/tests/support/example_schema.toml +++ b/cosmos-stdtx/tests/support/example_schema.toml @@ -7,6 +7,10 @@ # (e.g. `cosmos-sdk/StdTx` for Cosmos SDK) namespace = "core/StdTx" +# Bech32 address prefixes +acc_prefix = "terra" +val_prefix = "terravaloper" + [[definition]] type_name = "oracle/MsgExchangeRatePrevote" fields = [ From bd6b7cd7a8c6bf7653fbaeaa5c5f9b639d22ef21 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Mon, 27 Jan 2020 11:28:00 -0800 Subject: [PATCH 5/5] Implement cosmos_stdtx::stdtx::Builder This commit contains the initial implementation of the `stdtx::Builder` type with support for constructing, signing, and serializing `StdTx` messages as Amino bytes. Also includes a comprehensive usage example for how to build a real-world transaction. --- Cargo.lock | 1 + cosmos-stdtx/Cargo.toml | 3 + cosmos-stdtx/src/decimal.rs | 130 ++++++++++++++++++++++++++++ cosmos-stdtx/src/error.rs | 27 +++++- cosmos-stdtx/src/lib.rs | 137 ++++++++++++++++++++++++++++-- cosmos-stdtx/src/msg.rs | 7 +- cosmos-stdtx/src/msg/builder.rs | 35 ++++---- cosmos-stdtx/src/msg/value.rs | 8 +- cosmos-stdtx/src/schema.rs | 37 +++++++- cosmos-stdtx/src/stdtx.rs | 18 +++- cosmos-stdtx/src/stdtx/builder.rs | 41 ++++++--- cosmos-stdtx/src/type_name.rs | 9 ++ 12 files changed, 403 insertions(+), 50 deletions(-) create mode 100644 cosmos-stdtx/src/decimal.rs diff --git a/Cargo.lock b/Cargo.lock index 7cc8482..1ad0573 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,6 +394,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "signatory-secp256k1", "subtle-encoding 0.5.0", "thiserror", "toml", diff --git a/cosmos-stdtx/Cargo.toml b/cosmos-stdtx/Cargo.toml index c5ef79c..88ce7ec 100644 --- a/cosmos-stdtx/Cargo.toml +++ b/cosmos-stdtx/Cargo.toml @@ -25,3 +25,6 @@ sha2 = "0.8" subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" toml = "0.5" + +[dev-dependencies] +signatory-secp256k1 = "0.18" diff --git a/cosmos-stdtx/src/decimal.rs b/cosmos-stdtx/src/decimal.rs new file mode 100644 index 0000000..b5a4c1f --- /dev/null +++ b/cosmos-stdtx/src/decimal.rs @@ -0,0 +1,130 @@ +//! Decimal type providing equivalent semantics to Cosmos [`sdk.Dec`] +//! +//! [`sdk.Dec`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Dec + +use crate::error::{Error, ErrorKind}; +use anomaly::{ensure, fail}; +use std::{ + convert::{TryFrom, TryInto}, + fmt::{self, Debug, Display}, + str::FromStr, +}; + +/// Number of decimal places used by `sdk.Dec` +/// See: +pub const PRECISION: u32 = 18; + +/// Maximum value of the decimal part of an `sdk.Dec` +pub const FRACTIONAL_DIGITS_MAX: u64 = 9_999_999_999_999_999_999; + +/// Decimal type which follows Cosmos [`sdk.Dec`] conventions. +/// +/// [`sdk.Dec`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Dec +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct Decimal(rust_decimal::Decimal); + +impl Decimal { + /// Create a new [`Decimal`] with the given whole number and decimal + /// parts. The decimal part assumes 18 digits of precision e.g. a + /// decimal with `(1, 1)` is `1.000000000000000001`. + /// + /// 18 digits required by the Cosmos SDK. See: + /// See: + pub fn new(integral_digits: i64, fractional_digits: u64) -> Result { + ensure!( + fractional_digits <= FRACTIONAL_DIGITS_MAX, + ErrorKind::Decimal, + "fractional digits exceed available precision: {}", + fractional_digits + ); + + let integral_digits: rust_decimal::Decimal = integral_digits.into(); + let fractional_digits: rust_decimal::Decimal = fractional_digits.into(); + let precision_exp: rust_decimal::Decimal = 10u64.pow(PRECISION).into(); + + let mut combined_decimal = (integral_digits * precision_exp) + fractional_digits; + combined_decimal.set_scale(PRECISION)?; + Ok(Decimal(combined_decimal)) + } + + /// Serialize this [`Decimal`] as Amino-encoded bytes + pub fn to_amino_bytes(mut self) -> Vec { + self.0 + .set_scale(0) + .expect("can't rescale decimal for Amino serialization"); + self.to_string().into_bytes() + } +} + +impl Debug for Decimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} + +impl Display for Decimal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for Decimal { + type Err = Error; + + fn from_str(s: &str) -> Result { + s.parse::()?.try_into() + } +} + +impl TryFrom for Decimal { + type Error = Error; + + fn try_from(mut decimal_value: rust_decimal::Decimal) -> Result { + match decimal_value.scale() { + 0 => { + let exp: rust_decimal::Decimal = 10u64.pow(PRECISION).into(); + decimal_value *= exp; + decimal_value.set_scale(PRECISION)?; + } + PRECISION => (), + other => fail!( + ErrorKind::Decimal, + "invalid decimal precision: {} (must be 0 or 18)", + other + ), + } + + Ok(Decimal(decimal_value)) + } +} + +macro_rules! impl_from_primitive_int_for_decimal { + ($($int:ty),+) => { + $(impl From<$int> for Decimal { + fn from(num: $int) -> Decimal { + Decimal::new(num as i64, 0).unwrap() + } + })+ + }; +} + +impl_from_primitive_int_for_decimal!(i8, i16, i32, i64, isize); +impl_from_primitive_int_for_decimal!(u8, u16, u32, u64, usize); + +#[cfg(test)] +mod tests { + use super::Decimal; + + /// Used by e.g. JSON + #[test] + fn string_serialization_test() { + let num = Decimal::from(-1i8); + assert_eq!(num.to_string(), "-1.000000000000000000") + } + + #[test] + fn amino_serialization_test() { + let num = Decimal::from(-1i8); + assert_eq!(b"-1000000000000000000", num.to_amino_bytes().as_slice()); + } +} diff --git a/cosmos-stdtx/src/error.rs b/cosmos-stdtx/src/error.rs index 36c1e48..a9c8dd3 100644 --- a/cosmos-stdtx/src/error.rs +++ b/cosmos-stdtx/src/error.rs @@ -14,6 +14,10 @@ pub enum ErrorKind { #[error("address error")] Address, + /// Invalid decimal value + #[error("invalid decimal value")] + Decimal, + /// Input/output errors #[error("I/O error")] Io, @@ -22,6 +26,10 @@ pub enum ErrorKind { #[error("parse error")] Parse, + /// Signature error + #[error("signature error")] + Signature, + /// Invalid type #[error("type error")] Type, @@ -70,14 +78,25 @@ impl From> for Error { } } +impl From for Error { + fn from(err: rust_decimal::Error) -> Error { + Context::new(ErrorKind::Decimal, Some(err.into())).into() + } +} + +impl From for Error { + fn from(err: ecdsa::signature::Error) -> Error { + Context::new(ErrorKind::Signature, Some(err.into())).into() + } +} impl From for Error { - fn from(source: subtle_encoding::Error) -> Error { - Context::new(ErrorKind::Parse, Some(source.into())).into() + fn from(err: subtle_encoding::Error) -> Error { + Context::new(ErrorKind::Parse, Some(err.into())).into() } } impl From for Error { - fn from(source: toml::de::Error) -> Error { - Context::new(ErrorKind::Parse, Some(source.into())).into() + fn from(err: toml::de::Error) -> Error { + Context::new(ErrorKind::Parse, Some(err.into())).into() } } diff --git a/cosmos-stdtx/src/lib.rs b/cosmos-stdtx/src/lib.rs index bc1d49f..34a7974 100644 --- a/cosmos-stdtx/src/lib.rs +++ b/cosmos-stdtx/src/lib.rs @@ -1,22 +1,149 @@ -//! Amino serializer for Cosmos SDK-formatted `StdTx` transactions. +//! Amino serializer for Cosmos SDK-formatted `StdTx` transactions, the +//! standard transaction format used by the Cosmos SDK and other Tendermint +//! blockchains which reuse types from the Cosmos SDK. //! -//! This serializer accepts a TOML-based `sdk.Msg` schema as input, along -//! with a JSON +//! Uses a TOML-based schema description language for `sdk.Msg` values which +//! should be encoded into the final `StdTx`. +//! +//! Includes a `StdTx` builder capable of constructing `sdk.Msg` values and +//! signing them using any ECDSA secp256k1 signer compatible with the +//! [`ecdsa` crate] (e.g. [`signatory-secp256k1`], [`yubihsm`]). //! //! # Equivalent Go code //! //! - [`StdTx` (godoc)](https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx) -//! - [`sdk.Msg` (godoc)](https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg) +//! - [`sdk.Msg` (godoc)](httpshttps://docs.rs/ecdsa://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg) +//! +//! # Usage +//! +//! Below is a self-contained example of how to use [`cosmos_stdtx::Builder`] +//! type to construct a signed [`StdTx`] message: +//! +//! ``` +//! use cosmos_stdtx::Builder; +//! use signatory_secp256k1::{SecretKey, EcdsaSigner}; +//! +//! /// Example account number +//! const ACCOUNT_NUMBER: u64 = 946827; +//! +//! /// Example chain ID +//! const CHAIN_ID: &str = "columbus-3"; +//! +//! /// Example oracle feeder for `oracle/MsgExchangeRateVote` +//! const FEEDER: &str = "terra1t9et8wjeh8d0ewf4lldchterxsmhpcgg5auy47"; +//! +//! /// Example oracle validator for `oracle/MsgExchangeRateVote` +//! const VALIDATOR: &str = "terravaloper1grgelyng2v6v3t8z87wu3sxgt9m5s03x2mfyu7"; +//! +//! /// Example amount of gas to include in transaction +//! const GAS_AMOUNT: u64 = 200000; +//! +//! /// Example StdTx message schema definition. See docs for the +//! /// `cosmos_stdtx::Schema` type for more information: +//! /// +//! /// +//! /// Message types taken from Terra's oracle voter transactions: +//! /// +//! pub const TERRA_SCHEMA: &str = r#" +//! namespace = "core/StdTx" +//! acc_prefix = "terra" +//! val_prefix = "terravaloper" +//! +//! [[definition]] +//! type_name = "oracle/MsgExchangeRatePrevote" +//! fields = [ +//! { name = "hash", type = "string" }, +//! { name = "denom", type = "string" }, +//! { name = "feeder", type = "sdk.AccAddress" }, +//! { name = "validator", type = "sdk.ValAddress" }, +//! ] +//! +//! [[definition]] +//! type_name = "oracle/MsgExchangeRateVote" +//! fields = [ +//! { name = "exchange_rate", type = "sdk.Dec"}, +//! { name = "salt", type = "string" }, +//! { name = "denom", type = "string" }, +//! { name = "feeder", type = "sdk.AccAddress" }, +//! { name = "validator", type = "sdk.ValAddress" }, +//! ] +//! "#; +//! +//! /// Simple error type +//! #[derive(Debug)] +//! struct Error(String); +//! +//! impl From for Error { +//! fn from(err: cosmos_stdtx::Error) -> Error { +//! Error(err.to_string()) +//! } +//! } +//! +//! /// Simple builder for an `oracle/MsgExchangeRateVote` message +//! fn build_vote_msg(schema: &cosmos_stdtx::Schema) -> Result { +//! Ok(cosmos_stdtx::msg::Builder::new(schema, "oracle/MsgExchangeRateVote")? +//! .decimal("exchange_rate", -1i8)? +//! .string("salt", "XXXX")? +//! .string("denom", "ukrw")? +//! .acc_address_bech32("feeder", FEEDER)? +//! .val_address_bech32("validator", VALIDATOR)? +//! .to_msg()) +//! } +//! +//! /// Parse the TOML schema for Terra `sdk.Msg` types +//! let schema = TERRA_SCHEMA.parse::().unwrap(); +//! +//! /// Create ECDSA signer (ordinarily you wouldn't generate a random key +//! /// every time but reuse an existing one) +//! let signer = EcdsaSigner::from(&SecretKey::generate()); +//! +//! /// Create message builder, giving it an account number, chain ID, and a +//! /// boxed ECDSA secp256k1 signer +//! let builder = cosmos_stdtx::Builder::new(schema, ACCOUNT_NUMBER, CHAIN_ID, Box::new(signer)); +//! +//! /// Create message to be included in the `StdTx` using the method defined above +//! let msg = build_vote_msg(builder.schema()).unwrap(); +//! +//! /// Build transaction, returning serialized Amino bytes as a `Vec` +//! let sequence_number = 123456; +//! let fee = cosmos_stdtx::StdFee::for_gas(GAS_AMOUNT); +//! let memo = ""; +//! let amino_bytes = builder +//! .sign_amino_tx(sequence_number, fee, memo, &[msg]) +//! .unwrap(); +//! +//! // `amino_bytes` is now a `Vec` containing an Amino serialized transaction +//! ``` +//! +//! [`ecdsa` crate]: https://docs.rs/ecdsa +//! [`signatory-secp256k1`]: https://docs.rs/signatory-secp256k1 +//! [`yubihsm`]: https://docs.rs/yubihsm +//! [`cosmos_stdtx::Builder`]: https://docs.rs/cosmos-stdtx/latest/cosmos_stdtx/stdtx/struct.Builder.html #![doc(html_root_url = "https://docs.rs/cosmos-stdtx/0.0.1")] #![forbid(unsafe_code)] #![warn(rust_2018_idioms, missing_docs, unused_qualifications)] pub mod address; +pub mod decimal; pub mod error; pub mod msg; pub mod schema; pub mod stdtx; pub mod type_name; -pub use self::{address::Address, error::Error, msg::Msg, schema::Schema, type_name::TypeName}; +pub use self::{ + address::Address, + decimal::Decimal, + error::Error, + msg::Msg, + schema::Schema, + stdtx::{Builder, StdFee, StdTx}, + type_name::TypeName, +}; + +/// Fixed-width ECDSA secp256k1 signature +pub use ecdsa::curve::secp256k1::FixedSignature as Signature; + +/// Transaction signer for ECDSA secp256k1 signatures +pub type Signer = dyn ecdsa::signature::Signer; diff --git a/cosmos-stdtx/src/msg.rs b/cosmos-stdtx/src/msg.rs index c8a98a8..2f729b8 100644 --- a/cosmos-stdtx/src/msg.rs +++ b/cosmos-stdtx/src/msg.rs @@ -1,11 +1,12 @@ -//! Transaction message type (i.e `sdk.Msg`) +//! Transaction message type i.e [`sdk.Msg`] +//! +//! [`sdk.Msg`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg mod builder; mod field; mod value; pub use self::{builder::Builder, field::Field, value::Value}; -pub use rust_decimal::Decimal; use crate::{Schema, TypeName}; use prost_amino::encode_length_delimiter as encode_leb128; // Little-endian Base 128 @@ -14,7 +15,7 @@ use std::{collections::BTreeMap, iter::FromIterator}; /// Tags are indexes which identify message fields pub type Tag = u64; -/// Transaction message type (i.e. [`sdk.Msg`]). +/// Transaction message type i.e. [`sdk.Msg`]. /// These serve as the payload for [`StdTx`] transactions. /// /// [`StdTx`]: https://godoc.org/github.com/cosmos/cosmos-sdk/x/auth/types#StdTx diff --git a/cosmos-stdtx/src/msg/builder.rs b/cosmos-stdtx/src/msg/builder.rs index 38d5d47..43e262a 100644 --- a/cosmos-stdtx/src/msg/builder.rs +++ b/cosmos-stdtx/src/msg/builder.rs @@ -3,12 +3,12 @@ use super::{Field, Msg, Value}; use crate::{ address::Address, + decimal::Decimal, error::{Error, ErrorKind}, schema::{Definition, Schema, ValueType}, type_name::TypeName, }; use anomaly::{ensure, format_err}; -use rust_decimal::Decimal; use std::convert::TryInto; /// Transaction message builder @@ -58,14 +58,15 @@ impl<'a> Builder<'a> { /// pub fn acc_address( &mut self, - field_name: &TypeName, + field_name: impl TryInto, address: Address, ) -> Result<&mut Self, Error> { + let field_name = field_name.try_into()?; let tag = self .schema_definition - .get_field_tag(field_name, ValueType::SdkAccAddress)?; + .get_field_tag(&field_name, ValueType::SdkAccAddress)?; - let field = Field::new(tag, field_name.clone(), Value::SdkAccAddress(address)); + let field = Field::new(tag, field_name, Value::SdkAccAddress(address)); self.fields.push(field); Ok(self) @@ -74,7 +75,7 @@ impl<'a> Builder<'a> { /// `sdk.AccAddress` encoded as Bech32 pub fn acc_address_bech32( &mut self, - field_name: &TypeName, + field_name: impl TryInto, addr_bech32: impl AsRef, ) -> Result<&mut Self, Error> { let (hrp, address) = Address::from_bech32(addr_bech32)?; @@ -94,14 +95,16 @@ impl<'a> Builder<'a> { /// s pub fn decimal( &mut self, - field_name: &TypeName, + field_name: impl TryInto, value: impl Into, ) -> Result<&mut Self, Error> { + let field_name = field_name.try_into()?; + let tag = self .schema_definition - .get_field_tag(field_name, ValueType::SdkDecimal)?; + .get_field_tag(&field_name, ValueType::SdkDecimal)?; - let field = Field::new(tag, field_name.clone(), Value::SdkDecimal(value.into())); + let field = Field::new(tag, field_name, Value::SdkDecimal(value.into())); self.fields.push(field); Ok(self) @@ -111,14 +114,15 @@ impl<'a> Builder<'a> { /// pub fn val_address( &mut self, - field_name: &TypeName, + field_name: impl TryInto, address: Address, ) -> Result<&mut Self, Error> { + let field_name = field_name.try_into()?; let tag = self .schema_definition - .get_field_tag(field_name, ValueType::SdkValAddress)?; + .get_field_tag(&field_name, ValueType::SdkValAddress)?; - let field = Field::new(tag, field_name.clone(), Value::SdkValAddress(address)); + let field = Field::new(tag, field_name, Value::SdkValAddress(address)); self.fields.push(field); Ok(self) @@ -127,7 +131,7 @@ impl<'a> Builder<'a> { /// `sdk.ValAddress` encoded as Bech32 pub fn val_address_bech32( &mut self, - field_name: &TypeName, + field_name: impl TryInto, addr_bech32: impl AsRef, ) -> Result<&mut Self, Error> { let (hrp, address) = Address::from_bech32(addr_bech32)?; @@ -146,14 +150,15 @@ impl<'a> Builder<'a> { /// Strings pub fn string( &mut self, - field_name: &TypeName, + field_name: impl TryInto, s: impl Into, ) -> Result<&mut Self, Error> { + let field_name = field_name.try_into()?; let tag = self .schema_definition - .get_field_tag(field_name, ValueType::String)?; + .get_field_tag(&field_name, ValueType::String)?; - let field = Field::new(tag, field_name.clone(), Value::String(s.into())); + let field = Field::new(tag, field_name, Value::String(s.into())); self.fields.push(field); Ok(self) diff --git a/cosmos-stdtx/src/msg/value.rs b/cosmos-stdtx/src/msg/value.rs index 3c0251c..73e346e 100644 --- a/cosmos-stdtx/src/msg/value.rs +++ b/cosmos-stdtx/src/msg/value.rs @@ -2,9 +2,9 @@ use crate::{ address::Address, + decimal::Decimal, schema::{Schema, ValueType}, }; -use rust_decimal::Decimal; /// Message values - data contained in fields of a message #[derive(Clone, Debug)] @@ -52,9 +52,8 @@ impl Value { pub(super) fn to_amino_bytes(&self) -> Vec { match self { Value::SdkAccAddress(addr) | Value::SdkValAddress(addr) => addr.as_ref().to_vec(), - // TODO(tarcieri): check that decimals are being encoded correctly - Value::SdkDecimal(decimal) => decimal.to_string().as_bytes().to_vec(), - Value::String(s) => s.as_bytes().to_vec(), + Value::SdkDecimal(decimal) => decimal.to_amino_bytes(), + Value::String(s) => s.clone().into_bytes(), } } @@ -62,7 +61,6 @@ impl Value { pub(super) fn to_json_value(&self, schema: &Schema) -> serde_json::Value { serde_json::Value::String(match self { Value::SdkAccAddress(addr) => addr.to_bech32(schema.acc_prefix()), - // TODO(tarcieri): check that decimals are being encoded correctly Value::SdkDecimal(decimal) => decimal.to_string(), Value::SdkValAddress(addr) => addr.to_bech32(schema.val_prefix()), Value::String(s) => s.clone(), diff --git a/cosmos-stdtx/src/schema.rs b/cosmos-stdtx/src/schema.rs index ebe0d4f..95674d9 100644 --- a/cosmos-stdtx/src/schema.rs +++ b/cosmos-stdtx/src/schema.rs @@ -1,4 +1,4 @@ -//! Amino schema for an `sdk.Msg`. +//! Amino schema for an [`sdk.Msg`]. //! //! Schema files are similar to Protobuf schemas, but use a TOML-based syntax. //! @@ -8,15 +8,41 @@ //! a type named `oracle/MsgExchangeRatePrevote`: //! //! ```toml +//! # Example StdTx message schema definition. +//! # +//! # Message types taken from Terra's oracle voter transactions: +//! # +//! +//! # StdTx namespace for schema definitions +//! # (e.g. `cosmos-sdk/StdTx` for Cosmos SDK) +//! namespace = "core/StdTx" +//! +//! # Bech32 address prefixes +//! acc_prefix = "terra" +//! val_prefix = "terravaloper" +//! //! [[definition]] //! type_name = "oracle/MsgExchangeRatePrevote" //! fields = [ -//! { name = "hash", type = "string", tag = "1" }, # tag will be inferred if unspecified +//! { name = "hash", type = "string" }, +//! { name = "denom", type = "string" }, +//! { name = "feeder", type = "sdk.AccAddress" }, +//! { name = "validator", type = "sdk.ValAddress" }, +//! ] +//! +//! [[definition]] +//! type_name = "oracle/MsgExchangeRateVote" +//! fields = [ +//! # explicit field tag example - will start from "1" otherwise +//! { name = "exchange_rate", type = "sdk.Dec", tag = 1 }, +//! { name = "salt", type = "string" }, //! { name = "denom", type = "string" }, //! { name = "feeder", type = "sdk.AccAddress" }, //! { name = "validator", type = "sdk.ValAddress" }, //! ] //! ``` +//! +//! [`sdk.Msg`]: https://godoc.org/github.com/cosmos/cosmos-sdk/types#Msg mod definition; mod field; @@ -41,7 +67,7 @@ use std::{fs, path::Path, str::FromStr}; #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] #[serde(deny_unknown_fields)] pub struct Schema { - /// `StdTx` namespace for schema + /// `StdTx` namespace for schema (e.g. `cosmos-sdk/StdTx`) namespace: TypeName, /// Bech32 prefix for account addresses @@ -84,6 +110,11 @@ impl Schema { } } + /// Get the transaction namespace for this schema (e.g. `cosmos-sdk/StdTx`) + pub fn namespace(&self) -> &TypeName { + &self.namespace + } + /// Get the Bech32 prefix for account addresses pub fn acc_prefix(&self) -> &str { self.acc_prefix.as_ref() diff --git a/cosmos-stdtx/src/stdtx.rs b/cosmos-stdtx/src/stdtx.rs index 1879d6e..5567cf5 100644 --- a/cosmos-stdtx/src/stdtx.rs +++ b/cosmos-stdtx/src/stdtx.rs @@ -4,7 +4,7 @@ mod builder; pub use self::builder::Builder; -use crate::type_name::TypeName; +use crate::{Signature, TypeName}; use prost_amino::{encode_length_delimiter, Message}; use prost_amino_derive::Message; use serde_json::json; @@ -56,6 +56,13 @@ pub struct StdFee { } impl StdFee { + /// Create a [`StdFee`] for a gas-only transaction + pub fn for_gas(gas: u64) -> Self { + StdFee { + amount: vec![], + gas, + } + } /// Compute `serde_json::Value` representing this fee pub fn to_json_value(&self) -> serde_json::Value { let amount = self @@ -104,3 +111,12 @@ pub struct StdSignature { #[prost_amino(bytes)] pub signature: Vec, } + +impl From for StdSignature { + fn from(signature: Signature) -> StdSignature { + StdSignature { + pub_key: vec![], + signature: signature.as_ref().to_vec(), + } + } +} diff --git a/cosmos-stdtx/src/stdtx/builder.rs b/cosmos-stdtx/src/stdtx/builder.rs index b02b1ef..c82f8a0 100644 --- a/cosmos-stdtx/src/stdtx/builder.rs +++ b/cosmos-stdtx/src/stdtx/builder.rs @@ -1,14 +1,9 @@ //! Builder for `StdTx` transactions which handles construction and signing. -pub use ecdsa::{curve::secp256k1::FixedSignature as Signature, signature::Signer as _}; - -use super::{StdFee, StdTx}; -use crate::{error::Error, msg::Msg, schema::Schema}; +use super::{StdFee, StdSignature, StdTx}; +use crate::{Error, Msg, Schema, Signer}; use serde_json::json; -/// Transaction signer -pub type Signer = dyn ecdsa::signature::Signer; - /// [`StdTx`] transaction builder, which handles construction, signing, and /// Amino serialization. pub struct Builder { @@ -31,13 +26,13 @@ impl Builder { schema: Schema, account_number: u64, chain_id: impl Into, - signer: impl Into>, + signer: Box, ) -> Self { Self { schema, account_number, chain_id: chain_id.into(), - signer: signer.into(), + signer, } } @@ -60,13 +55,31 @@ impl Builder { pub fn sign_tx( &self, sequence: u64, - fee: &StdFee, + fee: StdFee, memo: &str, messages: &[Msg], ) -> Result { - let sign_msg = self.create_sign_msg(sequence, fee, memo, messages); - let _signature = self.signer.sign(sign_msg.as_bytes()); - unimplemented!(); + let sign_msg = self.create_sign_msg(sequence, &fee, memo, messages); + let signature = StdSignature::from(self.signer.try_sign(sign_msg.as_bytes())?); + + Ok(StdTx { + msg: messages.iter().map(|msg| msg.to_amino_bytes()).collect(), + fee: Some(fee), + signatures: vec![signature], + memo: memo.to_owned(), + }) + } + + /// Build, sign, and encode a transaction in Amino format + pub fn sign_amino_tx( + &self, + sequence: u64, + fee: StdFee, + memo: &str, + messages: &[Msg], + ) -> Result, Error> { + let tx = self.sign_tx(sequence, fee, memo, messages)?; + Ok(tx.to_amino_bytes(self.schema.namespace())) } /// Create the JSON message to sign for this transaction @@ -77,7 +90,7 @@ impl Builder { .collect::>(); json!({ - "account_number": self.account_number, + "account_number": self.account_number.to_string(), "chain_id": self.chain_id, "fee": fee.to_json_value(), "memo": memo, diff --git a/cosmos-stdtx/src/type_name.rs b/cosmos-stdtx/src/type_name.rs index eab5b8d..e28468f 100644 --- a/cosmos-stdtx/src/type_name.rs +++ b/cosmos-stdtx/src/type_name.rs @@ -5,6 +5,7 @@ use anomaly::fail; use serde::{de, Deserialize}; use sha2::{Digest, Sha256}; use std::{ + convert::TryFrom, fmt::{self, Display}, str::FromStr, }; @@ -76,3 +77,11 @@ impl FromStr for TypeName { Ok(TypeName(s.to_owned())) } } + +impl TryFrom<&str> for TypeName { + type Error = Error; + + fn try_from(s: &str) -> Result { + s.parse() + } +}