use celestia_tendermint::block::{Commit, CommitSig, Header, Id};
use celestia_tendermint::signature::SIGNATURE_LENGTH;
use celestia_tendermint::vote;
use celestia_tendermint::{chain, Hash, Vote};
use crate::consts::{genesis::MAX_CHAIN_ID_LEN, version};
use crate::{bail_validation, Error, Result, ValidateBasic, ValidationError};
pub(crate) const GENESIS_HEIGHT: u64 = 1;
pub type Height = celestia_tendermint::block::Height;
impl ValidateBasic for Header {
fn validate_basic(&self) -> Result<(), ValidationError> {
if self.version.block != version::BLOCK_PROTOCOL {
bail_validation!(
"version block ({}) != block protocol ({})",
self.version.block,
version::BLOCK_PROTOCOL,
)
}
if self.chain_id.as_str().len() > MAX_CHAIN_ID_LEN {
bail_validation!(
"chain id ({}) len > maximum ({})",
self.chain_id,
MAX_CHAIN_ID_LEN
)
}
if self.height.value() == 0 {
bail_validation!("height == 0")
}
if self.height.value() == GENESIS_HEIGHT && self.last_block_id.is_some() {
bail_validation!("last_block_id == Some() at height {GENESIS_HEIGHT}");
}
if self.height.value() != GENESIS_HEIGHT && self.last_block_id.is_none() {
bail_validation!("last_block_id == None at height {}", self.height)
}
Ok(())
}
}
impl ValidateBasic for Commit {
fn validate_basic(&self) -> Result<(), ValidationError> {
if self.height.value() >= GENESIS_HEIGHT {
if is_zero(&self.block_id) {
bail_validation!("block_id is zero")
}
if self.signatures.is_empty() {
bail_validation!("no signatures in commit")
}
for commit_sig in &self.signatures {
commit_sig.validate_basic()?;
}
}
Ok(())
}
}
impl ValidateBasic for CommitSig {
fn validate_basic(&self) -> Result<(), ValidationError> {
match self {
CommitSig::BlockIdFlagAbsent => (),
CommitSig::BlockIdFlagCommit { signature, .. }
| CommitSig::BlockIdFlagNil { signature, .. } => {
if let Some(signature) = signature {
if signature.as_bytes().is_empty() {
bail_validation!("no signature in commit sig")
}
if signature.as_bytes().len() != SIGNATURE_LENGTH {
bail_validation!(
"signature ({:?}) length != required ({})",
signature.as_bytes(),
SIGNATURE_LENGTH
)
}
} else {
bail_validation!("no signature in commit sig")
}
}
}
Ok(())
}
}
pub trait CommitExt {
fn vote_sign_bytes(&self, chain_id: &chain::Id, signature_idx: usize) -> Result<Vec<u8>>;
}
impl CommitExt for Commit {
fn vote_sign_bytes(&self, chain_id: &chain::Id, signature_idx: usize) -> Result<Vec<u8>> {
let sig =
self.signatures
.get(signature_idx)
.cloned()
.ok_or(Error::InvalidSignatureIndex(
signature_idx,
self.height.value(),
))?;
let (validator_address, timestamp, signature) = match sig {
CommitSig::BlockIdFlagCommit {
validator_address,
timestamp,
signature,
}
| CommitSig::BlockIdFlagNil {
validator_address,
timestamp,
signature,
} => (validator_address, timestamp, signature),
CommitSig::BlockIdFlagAbsent => return Err(Error::UnexpectedAbsentSignature),
};
let vote = Vote {
vote_type: vote::Type::Precommit,
height: self.height,
round: self.round,
block_id: Some(self.block_id),
timestamp: Some(timestamp),
validator_address,
validator_index: signature_idx.try_into()?,
signature,
};
Ok(vote.to_signable_vec(chain_id.clone())?)
}
}
fn is_zero(id: &Id) -> bool {
matches!(id.hash, Hash::None)
&& matches!(id.part_set_header.hash, Hash::None)
&& id.part_set_header.total == 0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::HashExt;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
fn sample_commit() -> Commit {
serde_json::from_str(r#"{
"height": 1,
"round": 0,
"block_id": {
"hash": "17F7D5108753C39714DCA67E6A73CE855C6EA9B0071BBD4FFE5D2EF7F3973BFC",
"parts": {
"total": 1,
"hash": "BEEBB79CDA7D0574B65864D3459FAC7F718B82496BD7FE8B6288BF0A98C8EA22"
}
},
"signatures": [
{
"block_id_flag": 2,
"validator_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
"timestamp": "2023-06-23T10:40:48.769228056Z",
"signature": "HNn4c02eCt2+nGuBs55L8f3DAz9cgy9psLFuzhtg2XCWnlkt2V43TX2b54hQNi7C0fepBEteA3GC01aJM/JJCg=="
}
]
}"#).unwrap()
}
fn sample_header() -> Header {
serde_json::from_str(r#"{
"version": {
"block": "11",
"app": "1"
},
"chain_id": "private",
"height": "1",
"time": "2023-06-23T10:40:48.410305119Z",
"last_block_id": {
"hash": "",
"parts": {
"total": 0,
"hash": ""
}
},
"last_commit_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"data_hash": "3D96B7D238E7E0456F6AF8E7CDF0A67BD6CF9C2089ECB559C659DCAA1F880353",
"validators_hash": "64AEB6CA415A37540650FC04471974CE4FE88884CDD3300DF7BB27C1786871E9",
"next_validators_hash": "64AEB6CA415A37540650FC04471974CE4FE88884CDD3300DF7BB27C1786871E9",
"consensus_hash": "C0B6A634B72AE9687EA53B6D277A73ABA1386BA3CFC6D0F26963602F7F6FFCD6",
"app_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"last_results_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"evidence_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
"proposer_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53"
}"#).unwrap()
}
#[test]
fn block_id_is_zero() {
let mut block_id = sample_commit().block_id;
assert!(!is_zero(&block_id));
block_id.hash = Hash::None;
assert!(!is_zero(&block_id));
block_id.part_set_header.hash = Hash::None;
assert!(!is_zero(&block_id));
block_id.part_set_header.total = 0;
assert!(is_zero(&block_id));
}
#[test]
fn header_validate_basic() {
sample_header().validate_basic().unwrap();
}
#[test]
fn header_validate_invalid_block_version() {
let mut header = sample_header();
header.version.block = 1;
header.validate_basic().unwrap_err();
}
#[test]
fn header_validate_zero_height() {
let mut header = sample_header();
header.height = 0u32.into();
header.validate_basic().unwrap_err();
}
#[test]
fn header_validate_missing_last_block_id() {
let mut header = sample_header();
header.height = 2u32.into();
header.validate_basic().unwrap_err();
}
#[test]
fn header_validate_genesis_with_last_block_id() {
let mut header = sample_header();
header.last_block_id = Some(Id {
hash: Hash::default_sha256(),
..Id::default()
});
header.validate_basic().unwrap_err();
}
#[test]
fn commit_validate_basic() {
sample_commit().validate_basic().unwrap();
}
#[test]
fn commit_validate_invalid_block_id() {
let mut commit = sample_commit();
commit.block_id.hash = Hash::None;
commit.block_id.part_set_header.hash = Hash::None;
commit.block_id.part_set_header.total = 0;
commit.validate_basic().unwrap_err();
}
#[test]
fn commit_validate_no_signatures() {
let mut commit = sample_commit();
commit.signatures = vec![];
commit.validate_basic().unwrap_err();
}
#[test]
fn commit_validate_absent() {
let mut commit = sample_commit();
commit.signatures[0] = CommitSig::BlockIdFlagAbsent;
commit.validate_basic().unwrap();
}
#[test]
fn commit_validate_no_signature_in_sig() {
let mut commit = sample_commit();
let CommitSig::BlockIdFlagCommit {
validator_address,
timestamp,
..
} = commit.signatures[0].clone()
else {
unreachable!()
};
commit.signatures[0] = CommitSig::BlockIdFlagCommit {
signature: None,
timestamp,
validator_address,
};
commit.validate_basic().unwrap_err();
}
#[test]
fn vote_sign_bytes() {
let commit = sample_commit();
let signable_bytes = commit
.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 0)
.unwrap();
assert_eq!(
signable_bytes,
vec![
108u8, 8, 2, 17, 1, 0, 0, 0, 0, 0, 0, 0, 34, 72, 10, 32, 23, 247, 213, 16, 135, 83,
195, 151, 20, 220, 166, 126, 106, 115, 206, 133, 92, 110, 169, 176, 7, 27, 189, 79,
254, 93, 46, 247, 243, 151, 59, 252, 18, 36, 8, 1, 18, 32, 190, 235, 183, 156, 218,
125, 5, 116, 182, 88, 100, 211, 69, 159, 172, 127, 113, 139, 130, 73, 107, 215,
254, 139, 98, 136, 191, 10, 152, 200, 234, 34, 42, 12, 8, 176, 237, 213, 164, 6,
16, 152, 250, 229, 238, 2, 50, 7, 112, 114, 105, 118, 97, 116, 101
]
);
}
#[test]
fn vote_sign_bytes_absent_signature() {
let mut commit = sample_commit();
commit.signatures[0] = CommitSig::BlockIdFlagAbsent;
let res = commit.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 0);
assert!(matches!(res, Err(Error::UnexpectedAbsentSignature)));
}
#[test]
fn vote_sign_bytes_non_existent_signature() {
let commit = sample_commit();
let res = commit.vote_sign_bytes(&"private".to_owned().try_into().unwrap(), 3);
assert!(matches!(res, Err(Error::InvalidSignatureIndex(..))));
}
}