diff --git a/abci/src/lib.rs b/abci/src/lib.rs index 98d3abd..164b99a 100644 --- a/abci/src/lib.rs +++ b/abci/src/lib.rs @@ -25,8 +25,11 @@ pub use server::{start_server, CancellationToken, Server, ServerBuilder, ServerR pub use tenderdash_proto as proto; use tenderdash_proto::prost::{DecodeError, EncodeError}; +#[cfg(feature = "crypto")] +mod merkle; #[cfg(feature = "crypto")] pub mod signatures; + #[cfg(feature = "tracing-span")] /// Create tracing::Span for better logging pub mod tracing_span; diff --git a/abci/src/merkle.rs b/abci/src/merkle.rs new file mode 100644 index 0000000..607d6e7 --- /dev/null +++ b/abci/src/merkle.rs @@ -0,0 +1,71 @@ +//! Port of Merkle tree implementation in Tenderdash. +//! See https://github.com/dashpay/tenderdash/blob/feat/statesync-integration/crypto/merkle/tree.go +//! for original implementation. + +const LEAF_PREFIX: u8 = 0; +const INNER_PREFIX: u8 = 1; + +/// Calculate the Merkle root hash of a list of items. +pub(crate) fn merkle_hash>(items: &[T]) -> [u8; 32] { + match items.len() { + 0 => empty_hash(), + 1 => leaf(items[0].as_ref()), + _ => { + let k = get_split_point(items.len() as i64) as usize; + let left = merkle_hash(&items[..k]); + let right = merkle_hash(&items[k..]); + inner(left, right) + }, + } +} + +fn leaf(data: &[u8]) -> [u8; 32] { + let mut buf = vec![LEAF_PREFIX; 1]; + buf.extend(data); + lhash::sha256(&buf) +} + +fn inner(left: [u8; 32], right: [u8; 32]) -> [u8; 32] { + let mut buf = vec![INNER_PREFIX; 1]; + buf.extend(left); + buf.extend(right); + lhash::sha256(&buf) +} + +fn empty_hash() -> [u8; 32] { + return lhash::sha256(&[]); +} + +fn get_split_point(length: i64) -> i64 { + if length < 1 { + panic!("Trying to split a tree with size < 1"); + } + let u_length = length as u64; + let bitlen = 64 - u_length.leading_zeros(); + let mut k = 1 << (bitlen - 1); + if k == length as u64 { + k >>= 1; + } + k as i64 +} + +#[cfg(test)] +mod test { + use crate::merkle::merkle_hash; + + #[test] + /// Given a set of test vectors that are the same as on Tenderdash tests, + /// When calculating the Merkle root hash, + /// Then the result should be the same as the expected value generated in + /// Tenderdash. + /// + /// ## See also + /// + /// Tenderdash test [TestHashFromByteSlices](https://github.com/dashpay/tenderdash/blob/feat/statesync-integration/crypto/merkle/tree_test.go#L21) + fn test_merkle_root() { + let items = vec![vec![1, 2], vec![3, 4], vec![5, 6], vec![7, 8], vec![9, 10]]; + let root = merkle_hash(&items); + let expected = "f326493eceab4f2d9ffbc78c59432a0a005d6ea98392045c74df5d14a113be18"; + assert_eq!(hex::encode(root), expected) + } +} diff --git a/abci/src/signatures.rs b/abci/src/signatures.rs index 3004193..a19095a 100644 --- a/abci/src/signatures.rs +++ b/abci/src/signatures.rs @@ -23,12 +23,15 @@ use std::{ }; use bytes::BufMut; -use tenderdash_proto::{prost::Message, types::CanonicalVote}; use crate::{ - proto::types::{ - BlockId, CanonicalBlockId, CanonicalVoteExtension, Commit, SignedMsgType, StateId, Vote, - VoteExtension, VoteExtensionType, + merkle::merkle_hash, + proto::{ + prost::Message, + types::{ + BlockId, CanonicalBlockId, CanonicalVote, CanonicalVoteExtension, Commit, + SignedMsgType, StateId, ValidatorSet, Vote, VoteExtension, VoteExtensionType, + }, }, Error, }; @@ -420,6 +423,55 @@ impl SignBytes for VoteExtension { } } +impl Hashable for ValidatorSet { + /// Calculate hash of validator set. + /// + /// Calculates hash of validator set that is used in block header + /// [Header.validators_hash](crate::proto::types::Header::validators_hash) + /// and + /// [Header.next_validators_hash](crate::proto::types::Header::next_validators_hash) fields. + /// + /// ## Tenderdash implementation + /// + /// Original Tenderdash implementation: + /// ```go + /// bzs := make([][]byte, 2) + /// bzs[0] = vals.ThresholdPublicKey.Bytes() + /// bzs[1] = vals.QuorumHash + /// return merkle.HashFromByteSlices(bzs) + /// ``` + /// + /// ## Arguments + /// + /// All arguments are ignored. + /// + /// ## Returns + /// + /// Returns hash of validator set. + fn calculate_msg_hash( + &self, + _chain_id: &str, + _height: i64, + _round: i32, + ) -> Result, Error> { + use tenderdash_proto::crypto::public_key::Sum::*; + let threshold_public_key_enum = self + .threshold_public_key + .as_ref() + .ok_or(Error::Canonical("missing threshold public key".to_string()))?; + + let threshold_public_key = match &threshold_public_key_enum.sum { + Some(Bls12381(pk)) => pk, + Some(Ed25519(pk)) => pk, + Some(Secp256k1(pk)) => pk, + None => return Err(Error::Canonical("missing threshold public key".to_string())), + }; + + let result = merkle_hash(&[threshold_public_key, &self.quorum_hash]); + Ok(result.to_vec()) + } +} + #[cfg(test)] pub mod tests { use std::{string::ToString, vec::Vec}; @@ -429,7 +481,7 @@ pub mod tests { proto::types::{ Commit, PartSetHeader, SignedMsgType, Vote, VoteExtension, VoteExtensionType, }, - signatures::Signable, + signatures::{Hashable, Signable}, }; #[test] @@ -599,4 +651,35 @@ pub mod tests { assert_eq!(sign_hash, expected_sign_hash); } + + #[test] + fn test_validator_set_hash() { + use crate::proto::crypto::{public_key::Sum::Bls12381, PublicKey}; + + const QUORUM_HASH_HEX: &str = + "703ee5bfc78765cc9e151d8dd84e30e196ababa83ac6cbdee31a88a46bba81b9"; + const THRESHOLD_PUB_KEY_HEX: &str = + "830e45e45e6414d9d615473cc2814e6b171c508f9c77e8b16924b74594f61c9956a6fa16335e98467eac8d8bdb76d187"; + const VALIDATORS_HASH_HEX: &str = + "81742F95E99EAE96ABC727FE792CECB4996205DE6BFC88AFEE1F60B96BC648B2"; + + let pubkey_vec = hex::decode(THRESHOLD_PUB_KEY_HEX).unwrap(); + let quorum_hash = hex::decode(QUORUM_HASH_HEX).unwrap(); + let expected_validators_hash = hex::decode(VALIDATORS_HASH_HEX).unwrap(); + + let threshold_public_key = PublicKey { + sum: Some(Bls12381(pubkey_vec)), + }; + + let vs = crate::proto::types::ValidatorSet { + threshold_public_key: Some(threshold_public_key), + quorum_hash, + ..Default::default() + }; + + // chain_id, height and round are unused + let actual = vs.calculate_msg_hash("", 0, 0).unwrap(); + + assert_eq!(expected_validators_hash, actual,); + } }