Skip to content

Commit

Permalink
IBC-proto crate (#238)
Browse files Browse the repository at this point in the history
* IBC-proto move over

* proto-compiler crate

* DomainType copied from Tendermint-rs
  • Loading branch information
greg-szabo authored Sep 17, 2020
1 parent d0f8aa1 commit 4be6f3d
Show file tree
Hide file tree
Showing 40 changed files with 4,124 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ members = [
"modules",
"relayer",
"relayer-cli",
"proto"
]

exclude = [
"proto-compiler"
]
1 change: 1 addition & 0 deletions proto-compiler/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
18 changes: 18 additions & 0 deletions proto-compiler/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "ibc-proto-compiler"
version = "0.1.0"
authors = ["Greg Szabo <greg@philosobear.com>"]
edition = "2018"
publish = false

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
walkdir = { version = "2.3" }

[build-dependencies]
prost-build = { version = "0.6" }
walkdir = { version = "2.3" }
git2 = { version = "0.13" }

[workspace]
48 changes: 48 additions & 0 deletions proto-compiler/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use git2::Repository;
use std::env::var;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

fn main() {
let sdk_dir = var("SDK_DIR").unwrap_or_else(|_| "target/cosmos-sdk".to_string());
if !Path::new(&sdk_dir).exists() {
let url = "https://github.com/cosmos/cosmos-sdk";
Repository::clone(url, &sdk_dir).unwrap();
}

// Paths
let proto_paths = [
"../proto/definitions/mock".to_string(),
format!("{}/proto/ibc", sdk_dir),
format!("{}/proto/cosmos/tx", sdk_dir),
format!("{}/proto/cosmos/base", sdk_dir),
];
let proto_includes_paths = [
"../proto/definitions".to_string(),
format!("{}/proto", sdk_dir),
format!("{}/third_party/proto", sdk_dir),
];

// List available proto files
let mut protos: Vec<PathBuf> = vec![];
for proto_path in &proto_paths {
protos.append(
&mut WalkDir::new(proto_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.file_type().is_file()
&& e.path().extension().is_some()
&& e.path().extension().unwrap() == "proto"
})
.map(|e| e.into_path())
.collect(),
);
}

// List available paths for dependencies
let includes: Vec<PathBuf> = proto_includes_paths.iter().map(PathBuf::from).collect();

// Compile all proto files
prost_build::compile_protos(&protos, &includes).unwrap();
}
36 changes: 36 additions & 0 deletions proto-compiler/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::fs::remove_dir_all;
use std::fs::{copy, create_dir_all};
use walkdir::WalkDir;

pub(crate) fn main() {
let ibc_proto_path = "../proto/src/prost";

// Remove old compiled files
remove_dir_all(ibc_proto_path).unwrap_or_default();
create_dir_all(ibc_proto_path).unwrap();

// Copy new compiled files (prost does not use folder structures)
let err: Vec<std::io::Error> = WalkDir::new(env!("OUT_DIR"))
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| {
copy(
e.path(),
std::path::Path::new(&format!(
"{}/{}",
ibc_proto_path,
&e.file_name().to_os_string().to_str().unwrap()
)),
)
})
.filter_map(|e| e.err())
.collect();

if !err.is_empty() {
for e in err {
dbg!(e);
}
panic!("error while copying compiled files")
}
}
25 changes: 25 additions & 0 deletions proto/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "ibc-proto"
version = "0.3.0"
authors = ["Greg Szabo <greg@informal.systems>"]
edition = "2018"
license = "Apache-2.0"
repository = "https://github.com/informalsystems/ibc-proto/tree/master/ibc_proto"
readme = "README.md"
categories = ["cryptography", "cryptography::cryptocurrencies", "database"]
keywords = ["blockchain", "cosmos", "tendermint", "ibc", "proto"]
exclude = ["definitions"]

description = """
ibc-proto is a the Rust implementation of the Cosmos SDK proto structs.
"""

[package.metadata.docs.rs]
all-features = true

[dependencies]
prost = { version = "0.6" }
prost-types = { version = "0.6" }
anomaly = "0.2"
bytes = "0.5"
thiserror = "1.0"
53 changes: 53 additions & 0 deletions proto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## ibc-proto

[![Crate][crate-image]][crate-link]
[![Docs][docs-image]][docs-link]
[![Build Status][build-image]][build-link]
[![Audit Status][audit-image]][audit-link]
[![Apache 2.0 Licensed][license-image]][license-link]
![Rust 1.39+][rustc-image]

Rust crate for interacting with Cosmos SDK
[IBC structs](https://github.com/cosmos/cosmos-sdk/tree/master/proto/ibc).

[Documentation][docs-link]

## Requirements

- Rust 1.39+
- make, curl
- Cosmos SDK (downloaded automatically if you are using `make`)

## License

Copyright © 2020 Informal Systems

Licensed under the Apache License, Version 2.0 (the "License");
you may not use the files in this repository 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/ibc-proto.svg
[crate-link]: https://crates.io/crates/ibc-proto
[docs-image]: https://docs.rs/ibc-proto/badge.svg
[docs-link]: https://docs.rs/ibc-proto/
[build-image]: https://github.com/informalsystems/ibc-rs/workflows/Rust/badge.svg
[build-link]: https://github.com/informalsystems/ibc-rs/actions?query=workflow%3ARust
[audit-image]: https://github.com/informalsystems/ibc-rs/workflows/Audit-Check/badge.svg
[audit-link]: https://github.com/informalsystems/ibc-rs/actions?query=workflow%3AAudit-Check
[license-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg
[license-link]: https://github.com/informalsystems/ibc-rs/blob/master/LICENSE
[rustc-image]: https://img.shields.io/badge/rustc-1.39+-blue.svg

[//]: # (general links)

[Cosmos SDK]: https://github.com/cosmos/cosmos-sdk
12 changes: 12 additions & 0 deletions proto/definitions/mock/ibc.mock.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
syntax = "proto3";
package ibc.mock;

import "ibc/client/client.proto";

message Header {
ibc.client.Height height = 1;
}

message ClientState {
Header header = 1;
}
139 changes: 139 additions & 0 deletions proto/src/domaintype.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! DomainType trait
//!
//! The DomainType trait allows separation of the data sent on the wire (currently encoded using
//! protobuf) from the structures used in Rust. The structures used to encode/decode from/to the
//! wire are called "Raw" types (they mirror the definitions in the specifications) and the Rust
//! types we use internally are called the "Domain" types. These Domain types can implement
//! additional checks and conversions to consume the incoming data easier for a Rust developer.
//!
//! The benefits include decoding the wire into a struct that is inherently valid as well as hiding
//! the encoding and decoding details from the developer. This latter is important if/when we decide
//! to exchange the underlying Prost library with something else. (Another protobuf implementation
//! or a completely different encoding.) Encoding is not the core product of IBC it's a
//! necessary dependency.
//!
//!
//! Decode: bytestream -> Raw -> Domain
//! The `decode` function takes two steps to decode from a bytestream to a DomainType:
//!
//! 1. Decode the bytestream into a Raw type using the Prost library,
//! 2. Transform that Raw type into a Domain type using the TryFrom trait of the DomainType.
//!
//!
//! Encode: Domain -> Raw -> bytestream
//! The `encode` function takes two steps to encode a DomainType into a bytestream:
//!
//! 1. Transform the Domain type into a Raw type using the From trait of the DomainType,
//! 2. Encode the Raw type into a bytestream using the Prost library.
//!
//!
//! Note that in the case of encode, the transformation to Raw type is infallible:
//! Rust structs should always be ready to be encoded to the wire.
//!
//! Note that the Prost library and the TryFrom method have their own set of errors. These are
//! merged into a custom Error type defined in this crate for easier handling.
//!
//! Requirements:
//! * The DomainType trait requires the struct to implement the Clone trait.
//! * Any RawType structure implements the prost::Message trait. (protobuf struct)
//! * The DomainType trait requires that the TryFrom<RawType> implemented on the structure has an
//! error type that implements Into<BoxError>. (The current implementations with anomaly are
//! fine.)
//!
//! How to implement a DomainType struct:
//! 1. Implement your struct based on your expectations for the developer
//! 2. Add `impl DomainType<MyRawType> for MyDomainType {}` blanket implementation of the trait
//! 4. Implement the `TryFrom<MyRawType> for MyDomainType` trait
//! 5. Implement the `From<MyDomainType> for MyRawType` trait
use crate::{Error, Kind};
use anomaly::BoxError;
use bytes::{Buf, BufMut};
use prost::{encoding::encoded_len_varint, Message};
use std::convert::{TryFrom, TryInto};

/// DomainType trait allows protobuf encoding and decoding for domain types
pub trait DomainType<T: Message + From<Self> + Default>
where
Self: Sized + Clone + TryFrom<T>,
<Self as TryFrom<T>>::Error: Into<BoxError>,
{
/// Encodes the DomainType into a buffer.
///
/// This function replaces the Prost::Message encode() function for DomainTypes.
fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), Error> {
T::from(self.clone())
.encode(buf)
.map_err(|e| Kind::EncodeMessage.context(e).into())
}

/// Encodes the DomainType with a length-delimiter to a buffer.
///
/// An error will be returned if the buffer does not have sufficient capacity.
///
/// This function replaces the Prost::Message encode_length_delimited() function for
/// DomainTypes.
fn encode_length_delimited<B: BufMut>(&self, buf: &mut B) -> Result<(), Error> {
T::from(self.clone())
.encode_length_delimited(buf)
.map_err(|e| Kind::EncodeMessage.context(e).into())
}

/// Decodes an instance of the message from a buffer and then converts it into DomainType.
///
/// The entire buffer will be consumed.
///
/// This function replaces the Prost::Message decode() function for DomainTypes.
fn decode<B: Buf>(buf: B) -> Result<Self, Error> {
T::decode(buf).map_or_else(
|e| Err(Kind::DecodeMessage.context(e).into()),
|t| Self::try_from(t).map_err(|e| Kind::TryIntoDomainType.context(e).into()),
)
}

/// Decodes a length-delimited instance of the message from the buffer.
///
/// The entire buffer will be consumed.
///
/// This function replaces the Prost::Message decode_length_delimited() function for
/// DomainTypes.
fn decode_length_delimited<B: Buf>(buf: B) -> Result<Self, Error> {
T::decode_length_delimited(buf).map_or_else(
|e| Err(Kind::DecodeMessage.context(e).into()),
|t| Self::try_from(t).map_err(|e| Kind::TryIntoDomainType.context(e).into()),
)
}

/// Returns the encoded length of the message without a length delimiter.
///
/// This function replaces the Prost::Message encoded_len() function for DomainTypes.
fn encoded_len(&self) -> usize {
T::from(self.clone()).encoded_len()
}

/// Encodes the DomainType into a protobuf-encoded Vec<u8>
fn encode_vec(&self) -> Result<Vec<u8>, Error> {
let mut wire = Vec::with_capacity(self.encoded_len());
self.encode(&mut wire).map(|_| wire)
}

/// Decodes a protobuf-encoded instance of the message from a Vec<u8> and then converts it into
/// DomainType.
fn decode_vec(v: &[u8]) -> Result<Self, Error> {
Self::decode(v)
}

/// Encodes the DomainType with a length-delimiter to a Vec<u8> protobuf-encoded message.
fn encode_length_delimited_vec(&self) -> Result<Vec<u8>, Error> {
let len = self.encoded_len();
let lenu64 = len.try_into().map_err(|e| Kind::EncodeMessage.context(e))?;
let mut wire = Vec::with_capacity(len + encoded_len_varint(lenu64));
self.encode_length_delimited(&mut wire).map(|_| wire)
}

/// Decodes a protobuf-encoded instance of the message with a length-delimiter from a Vec<u8>
/// and then converts it into DomainType.
fn decode_length_delimited_vec(v: &[u8]) -> Result<Self, Error> {
Self::decode_length_delimited(v)
}
}
37 changes: 37 additions & 0 deletions proto/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! This module defines the various errors that be raised during DomainType conversions.
use anomaly::{BoxError, Context};
use thiserror::Error;

/// An error that can be raised by the DomainType conversions.
pub type Error = anomaly::Error<Kind>;

/// Various kinds of errors that can be raised.
#[derive(Clone, Debug, Error)]
pub enum Kind {
/// TryFrom Prost Message failed during decoding
#[error("error converting message type into domain type")]
TryIntoDomainType,

/// encoding prost Message into buffer failed
#[error("error encoding message into buffer")]
EncodeMessage,

/// decoding buffer into prost Message failed
#[error("error decoding buffer into message")]
DecodeMessage,
}

impl Kind {
/// Add a given source error as context for this error kind
///
/// This is typically use with `map_err` as follows:
///
/// ```ignore
/// let x = self.something.do_stuff()
/// .map_err(|e| error::Kind::Config.context(e))?;
/// ```
pub fn context(self, source: impl Into<BoxError>) -> Context<Self> {
Context::new(self, Some(source.into()))
}
}
Loading

0 comments on commit 4be6f3d

Please sign in to comment.