From be27fdf1d68d5f5c98099fd609c1bd110646a836 Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Mon, 27 Jan 2020 06:19:28 -0800 Subject: [PATCH] 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 = [