Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(proto): add sign_digest() to commit and vote extension #20

Merged
merged 12 commits into from
Apr 17, 2023
12 changes: 11 additions & 1 deletion proto-compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,20 @@ pub fn proto_compile() {
&commitish,
); // This panics if it fails.

// We need all files in proto/tendermint/abci, plus .../types/canonical.proto
// for signature verification
let proto_paths = vec![tenderdash_dir.join("proto").join("tendermint").join("abci")];
let proto_includes_paths = vec![tenderdash_dir.join("proto"), thirdparty_dir];
// List available proto files
let protos = find_proto_files(proto_paths);
let mut protos = find_proto_files(proto_paths);
// On top of that, we add canonical.proto, required to verify signatures
protos.push(
tenderdash_dir
.join("proto")
.join("tendermint")
.join("types")
.join("canonical.proto"),
);

let mut pb = prost_build::Config::new();

Expand Down
10 changes: 9 additions & 1 deletion proto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,19 @@ time = { version = "0.3", default-features = false, features = [
"macros",
"parsing",
] }
flex-error = { version = "0.4.4", default-features = false }
thiserror = { version = "1.0.40" }
chrono = { version = "0.4.24", default-features = false }
derive_more = { version = "0.99.17" }
lhash = { version = "1.0.1", features = ["sha256"], optional = true }

[dev-dependencies]
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
hex = { version = "0.4" }

[build-dependencies]
tenderdash-proto-compiler = { path = "../proto-compiler" }


[features]
default = ["crypto"]
crypto = ["dep:lhash"]
44 changes: 20 additions & 24 deletions proto/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
//! This module defines the various errors that be raised during Protobuf
//! conversions.

extern crate std;
use core::{convert::TryFrom, fmt::Display, num::TryFromIntError};

use flex_error::{define_error, DisplayOnly};
use prost::{DecodeError, EncodeError};

use crate::prelude::*;

define_error! {
Error {
TryFromProtobuf
{ reason: String }
| e | {
format!("error converting message type into domain type: {}",
e.reason)
},

EncodeMessage
[ DisplayOnly<EncodeError> ]
| _ | { "error encoding message into buffer" },

DecodeMessage
[ DisplayOnly<DecodeError> ]
| _ | { "error decoding buffer into message" },

ParseLength
[ DisplayOnly<TryFromIntError> ]
| _ | { "error parsing encoded length" },
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// error converting message type into domain type
#[error("error converting message type into domain type: {0}")]
TryFromProtobuf(String),

/// error encoding message into buffer
#[error("error encoding message into buffer: {0:?}")]
EncodeMessage(EncodeError),
/// error decoding buffer into message
#[error("error decoding buffer into message: {0:?}")]
DecodeMessage(DecodeError),
/// error decoding buffer into message
#[error("error building canonical message: {0}")]
CreateCanonical(String),
/// error parsing encoded length
#[error("error parsing encoded length: {0:?}")]
ParseLength(TryFromIntError),
}

impl Error {
Expand All @@ -37,6 +33,6 @@ impl Error {
E: Display,
T: TryFrom<Raw, Error = E>,
{
Error::try_from_protobuf(format!("{e}"))
Error::TryFromProtobuf(format!("{e}"))
}
}
14 changes: 8 additions & 6 deletions proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ pub use tenderdash::*;

pub mod serializers;

use prelude::*;
#[cfg(feature = "crypto")]
pub mod signatures;

use prelude::*;
pub use tenderdash::meta::ABCI_VERSION;

/// Allows for easy Google Protocol Buffers encoding and decoding of domain
Expand Down Expand Up @@ -130,7 +132,7 @@ where
fn encode<B: BufMut>(&self, buf: &mut B) -> Result<(), Error> {
T::from(self.clone())
.encode(buf)
.map_err(Error::encode_message)
.map_err(Error::EncodeMessage)
}

/// Encode with a length-delimiter to a buffer in Protobuf format.
Expand All @@ -145,7 +147,7 @@ where
fn encode_length_delimited<B: BufMut>(&self, buf: &mut B) -> Result<(), Error> {
T::from(self.clone())
.encode_length_delimited(buf)
.map_err(Error::encode_message)
.map_err(Error::EncodeMessage)
}

/// Constructor that attempts to decode an instance from a buffer.
Expand All @@ -157,7 +159,7 @@ where
///
/// [`prost::Message::decode`]: https://docs.rs/prost/*/prost/trait.Message.html#method.decode
fn decode<B: Buf>(buf: B) -> Result<Self, Error> {
let raw = T::decode(buf).map_err(Error::decode_message)?;
let raw = T::decode(buf).map_err(Error::DecodeMessage)?;

Self::try_from(raw).map_err(Error::try_from::<T, Self, _>)
}
Expand All @@ -172,7 +174,7 @@ where
///
/// [`prost::Message::decode_length_delimited`]: https://docs.rs/prost/*/prost/trait.Message.html#method.decode_length_delimited
fn decode_length_delimited<B: Buf>(buf: B) -> Result<Self, Error> {
let raw = T::decode_length_delimited(buf).map_err(Error::decode_message)?;
let raw = T::decode_length_delimited(buf).map_err(Error::DecodeMessage)?;

Self::try_from(raw).map_err(Error::try_from::<T, Self, _>)
}
Expand Down Expand Up @@ -202,7 +204,7 @@ where
/// Encode 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(Error::parse_length)?;
let lenu64 = len.try_into().map_err(Error::ParseLength)?;
let mut wire = Vec::with_capacity(len + encoded_len_varint(lenu64));
self.encode_length_delimited(&mut wire).map(|_| wire)
}
Expand Down
197 changes: 197 additions & 0 deletions proto/src/signatures.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use alloc::{string::String, vec::Vec};

use bytes::BufMut;
use prost::Message;

use crate::{
types::{BlockId, CanonicalBlockId, Commit, SignedMsgType, StateId, Vote},
Error,
};

#[derive(Clone, Debug)]
pub struct SignContext {
pub chain_id: String,
}

impl SignContext {}
pub trait SignBytes {
/// Generate bytes that will be signed.
fn sign_bytes(&self, ctx: &SignContext) -> Result<Vec<u8>, Error>;

/// Generate hash of data to sign
fn sha256(&self, ctx: &SignContext) -> Result<Vec<u8>, Error> {
// todo!()
let sb = self.sign_bytes(ctx)?;
let result = lhash::sha256(&sb);
Ok(Vec::from(result))
}
}

impl SignBytes for StateId {
fn sign_bytes(&self, _ctx: &SignContext) -> Result<Vec<u8>, Error> {
let mut buf = Vec::new();
self.encode_length_delimited(&mut buf)
.map_err(Error::EncodeMessage)?;

Ok(buf.to_vec())
}
}

impl SignBytes for BlockId {
fn sign_bytes(&self, _ctx: &SignContext) -> Result<Vec<u8>, Error> {
let part_set_header = self.part_set_header.clone().unwrap_or_default();

let block_id = CanonicalBlockId {
hash: self.hash.clone(),
part_set_header: Some(crate::types::CanonicalPartSetHeader {
total: part_set_header.total,
hash: part_set_header.hash,
}),
};
let mut buf = Vec::new();
block_id
.encode_length_delimited(&mut buf)
.map_err(Error::EncodeMessage)?;

Ok(buf)
}
}

impl SignBytes for Vote {
fn sign_bytes(&self, ctx: &SignContext) -> Result<Vec<u8>, Error> {
let block_id = self
.block_id
.clone()
.ok_or(Error::CreateCanonical(String::from(
"missing vote.block id",
)))?;

vote_sign_bytes(ctx, block_id, self.r#type, self.height, self.round as i64)
}
}

impl SignBytes for Commit {
fn sign_bytes(&self, ctx: &SignContext) -> Result<Vec<u8>, Error> {
// we just use some rough guesstimate of intial capacity
let block_id = self
.block_id
.clone()
.ok_or(Error::CreateCanonical(String::from(
"missing vote.block id",
)))?;

vote_sign_bytes(
ctx,
block_id,
SignedMsgType::Precommit.into(),
self.height,
self.round as i64,
)
}
}

/// Generate sign bytes for a vote / commit
///
/// See https://github.com/dashpay/tenderdash/blob/bcb623bcf002ac54b26ed1324b98116872dd0da7/proto/tendermint/types/types.go#L56
fn vote_sign_bytes(
ctx: &SignContext,
block_id: BlockId,
vote_type: i32,
height: i64,
round: i64,
) -> Result<Vec<u8>, Error> {
// we just use some rough guesstimate of intial capacity
let mut buf = Vec::with_capacity(80);

let state_id = block_id.state_id.clone();
let block_id = block_id.sha256(ctx)?;

buf.put_i32_le(vote_type.into());
Fixed Show fixed Hide fixed
buf.put_i64_le(height);
buf.put_i64_le(round);

buf.extend(block_id);
buf.extend(state_id);
buf.put(ctx.chain_id.as_bytes());

Ok(buf.to_vec())
}

#[cfg(test)]
pub mod tests {
use alloc::string::ToString;

use super::SignBytes;
use crate::types::{Commit, PartSetHeader, SignedMsgType, Vote};

#[test]
/// Compare sign bytes for Vote with sign bytes generated by Tenderdash and
/// put into `expect_sign_bytes`.
fn vote_sign_bytes() {
let h = [1u8, 2, 3, 4].repeat(8);

let state_id_hash =
hex::decode("d7509905b5407ee72dadd93b4ae70a24ad8a7755fc677acd2b215710a05cfc47")
.unwrap();
let expect_sign_bytes = hex::decode("0100000001000000000000000200000000000000fb\
7c89bf010a91d50f890455582b7fed0c346e53ab33df7da0bcd85c10fa92ead7509905b5407ee72dadd93b\
4ae70a24ad8a7755fc677acd2b215710a05cfc47736f6d652d636861696e")
.unwrap();

let vote = Vote {
r#type: SignedMsgType::Prevote as i32,
height: 1,
round: 2,
block_id: Some(crate::types::BlockId {
hash: h.clone(),
part_set_header: Some(PartSetHeader {
total: 1,
hash: h.clone(),
}),
state_id: state_id_hash,
}),
..Default::default()
};
let ctx = super::SignContext {
chain_id: "some-chain".to_string(),
};

let actual = vote.sign_bytes(&ctx).unwrap();

assert_eq!(expect_sign_bytes, actual);
}

#[test]
fn commit_sign_bytes() {
let h = [1u8, 2, 3, 4].repeat(8);

let state_id_hash =
hex::decode("d7509905b5407ee72dadd93b4ae70a24ad8a7755fc677acd2b215710a05cfc47")
.unwrap();
let expect_sign_bytes = hex::decode("0200000001000000000000000200000000000000fb7c89bf010a91d5\
0f890455582b7fed0c346e53ab33df7da0bcd85c10fa92ead7509905b5407ee72dadd93b4ae70a24ad8a7755fc677acd2b215710\
a05cfc47736f6d652d636861696e")
.unwrap();

let commit = Commit {
height: 1,
round: 2,
block_id: Some(crate::types::BlockId {
hash: h.clone(),
part_set_header: Some(PartSetHeader {
total: 1,
hash: h.clone(),
}),
state_id: state_id_hash,
}),
..Default::default()
};
let ctx = super::SignContext {
chain_id: "some-chain".to_string(),
};

let actual = commit.sign_bytes(&ctx).unwrap();

assert_eq!(expect_sign_bytes, actual);
}
}