use crate::error::CosmosError;
use sha2::{Digest, Sha256};
use tendermint_proto::types::{BlockId, Vote, Proposal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignedMsgType {
Unknown,
Prevote,
Precommit,
Proposal,
}
impl SignedMsgType {
pub fn from_i32(value: i32) -> Result<Self, CosmosError> {
match value {
0 => Ok(SignedMsgType::Unknown),
1 => Ok(SignedMsgType::Prevote),
2 => Ok(SignedMsgType::Precommit),
32 => Ok(SignedMsgType::Proposal),
_ => Err(CosmosError::InvalidVoteType(value)),
}
}
pub fn to_core_type(self) -> Option<nklave_core::state::validator::CosmosSignedMsgType> {
match self {
SignedMsgType::Prevote => Some(nklave_core::state::validator::CosmosSignedMsgType::Prevote),
SignedMsgType::Precommit => Some(nklave_core::state::validator::CosmosSignedMsgType::Precommit),
SignedMsgType::Proposal => Some(nklave_core::state::validator::CosmosSignedMsgType::Proposal),
SignedMsgType::Unknown => None,
}
}
}
impl From<SignedMsgType> for i32 {
fn from(msg_type: SignedMsgType) -> Self {
match msg_type {
SignedMsgType::Unknown => 0,
SignedMsgType::Prevote => 1,
SignedMsgType::Precommit => 2,
SignedMsgType::Proposal => 32,
}
}
}
#[derive(Debug, Clone)]
pub struct VoteInfo {
pub msg_type: SignedMsgType,
pub height: i64,
pub round: i32,
pub block_hash: Option<[u8; 32]>,
pub validator_address: Vec<u8>,
}
impl VoteInfo {
pub fn from_vote(vote: &Vote) -> Result<Self, CosmosError> {
let msg_type = SignedMsgType::from_i32(vote.r#type)?;
let block_hash = extract_block_hash(vote.block_id.as_ref());
Ok(Self {
msg_type,
height: vote.height,
round: vote.round,
block_hash,
validator_address: vote.validator_address.clone(),
})
}
}
#[derive(Debug, Clone)]
pub struct ProposalInfo {
pub height: i64,
pub round: i32,
pub pol_round: i32,
pub block_hash: [u8; 32],
}
impl ProposalInfo {
pub fn from_proposal(proposal: &Proposal) -> Result<Self, CosmosError> {
let block_id = proposal
.block_id
.as_ref()
.ok_or(CosmosError::MissingField("block_id"))?;
let block_hash = hash_block_id(block_id);
Ok(Self {
height: proposal.height,
round: proposal.round,
pol_round: proposal.pol_round,
block_hash,
})
}
}
pub fn extract_block_hash(block_id: Option<&BlockId>) -> Option<[u8; 32]> {
block_id.and_then(|bid| {
if bid.hash.is_empty() {
None
} else {
Some(hash_block_id(bid))
}
})
}
pub fn hash_block_id(block_id: &BlockId) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(&block_id.hash);
if let Some(ref part_set) = block_id.part_set_header {
hasher.update(part_set.total.to_le_bytes());
hasher.update(&part_set.hash);
}
hasher.finalize().into()
}
pub fn canonical_vote_sign_bytes(
chain_id: &str,
vote: &Vote,
) -> Vec<u8> {
use prost::Message;
use tendermint_proto::types::CanonicalVote;
let canonical = CanonicalVote {
r#type: vote.r#type,
height: vote.height,
round: vote.round as i64,
block_id: vote.block_id.clone().map(|bid| {
tendermint_proto::types::CanonicalBlockId {
hash: bid.hash,
part_set_header: bid.part_set_header.map(|psh| {
tendermint_proto::types::CanonicalPartSetHeader {
total: psh.total,
hash: psh.hash,
}
}),
}
}),
timestamp: vote.timestamp.clone(),
chain_id: chain_id.to_string(),
};
let mut buf = Vec::new();
canonical.encode_length_delimited(&mut buf).expect("encoding should not fail");
buf
}
pub fn canonical_proposal_sign_bytes(
chain_id: &str,
proposal: &Proposal,
) -> Vec<u8> {
use prost::Message;
use tendermint_proto::types::CanonicalProposal;
let canonical = CanonicalProposal {
r#type: proposal.r#type,
height: proposal.height,
round: proposal.round as i64,
pol_round: proposal.pol_round as i64,
block_id: proposal.block_id.clone().map(|bid| {
tendermint_proto::types::CanonicalBlockId {
hash: bid.hash,
part_set_header: bid.part_set_header.map(|psh| {
tendermint_proto::types::CanonicalPartSetHeader {
total: psh.total,
hash: psh.hash,
}
}),
}
}),
timestamp: proposal.timestamp.clone(),
chain_id: chain_id.to_string(),
};
let mut buf = Vec::new();
canonical.encode_length_delimited(&mut buf).expect("encoding should not fail");
buf
}
#[cfg(test)]
mod tests {
use super::*;
use tendermint_proto::types::PartSetHeader;
#[test]
fn test_signed_msg_type_conversion() {
assert_eq!(SignedMsgType::from_i32(0).unwrap(), SignedMsgType::Unknown);
assert_eq!(SignedMsgType::from_i32(1).unwrap(), SignedMsgType::Prevote);
assert_eq!(SignedMsgType::from_i32(2).unwrap(), SignedMsgType::Precommit);
assert_eq!(SignedMsgType::from_i32(32).unwrap(), SignedMsgType::Proposal);
assert!(SignedMsgType::from_i32(99).is_err());
}
#[test]
fn test_extract_block_hash_nil() {
assert!(extract_block_hash(None).is_none());
let empty_bid = BlockId {
hash: vec![],
part_set_header: None,
};
assert!(extract_block_hash(Some(&empty_bid)).is_none());
}
#[test]
fn test_extract_block_hash_valid() {
let bid = BlockId {
hash: vec![1, 2, 3, 4],
part_set_header: Some(PartSetHeader {
total: 1,
hash: vec![5, 6, 7, 8],
}),
};
let hash = extract_block_hash(Some(&bid));
assert!(hash.is_some());
assert_eq!(hash.unwrap().len(), 32);
}
#[test]
fn test_hash_block_id_deterministic() {
let bid = BlockId {
hash: vec![1, 2, 3, 4],
part_set_header: Some(PartSetHeader {
total: 1,
hash: vec![5, 6, 7, 8],
}),
};
let hash1 = hash_block_id(&bid);
let hash2 = hash_block_id(&bid);
assert_eq!(hash1, hash2);
}
#[test]
fn test_vote_info_extraction() {
let vote = Vote {
r#type: 1, height: 100,
round: 0,
block_id: Some(BlockId {
hash: vec![1; 32],
part_set_header: None,
}),
timestamp: None,
validator_address: vec![0; 20],
validator_index: 0,
signature: vec![],
extension: vec![],
extension_signature: vec![],
};
let info = VoteInfo::from_vote(&vote).unwrap();
assert_eq!(info.msg_type, SignedMsgType::Prevote);
assert_eq!(info.height, 100);
assert_eq!(info.round, 0);
assert!(info.block_hash.is_some());
}
#[test]
fn test_canonical_vote_sign_bytes() {
let vote = Vote {
r#type: 1,
height: 100,
round: 0,
block_id: Some(BlockId {
hash: vec![1; 32],
part_set_header: None,
}),
timestamp: None,
validator_address: vec![],
validator_index: 0,
signature: vec![],
extension: vec![],
extension_signature: vec![],
};
let bytes = canonical_vote_sign_bytes("test-chain", &vote);
assert!(!bytes.is_empty());
let bytes2 = canonical_vote_sign_bytes("test-chain", &vote);
assert_eq!(bytes, bytes2);
}
}