use std::collections::HashMap;
use tendermint::block::CommitSig;
use tendermint::crypto::default::signature::Verifier;
use tendermint::validator::{Info, Set};
use tendermint::{account, block, chain};
use crate::block::CommitExt;
use crate::trust_level::TrustLevelRatio;
use crate::{
Result, ValidateBasic, ValidationError, VerificationError, bail_validation, bail_verification,
};
impl ValidateBasic for Set {
fn validate_basic(&self) -> Result<(), ValidationError> {
if self.validators().is_empty() {
bail_validation!("validatiors is empty")
}
if self.proposer().is_none() {
bail_validation!("proposer is none")
}
Ok(())
}
}
pub trait ValidatorSetExt {
fn verify_commit_light(
&self,
chain_id: &chain::Id,
height: &block::Height,
commit: &block::Commit,
) -> Result<()>;
fn verify_commit_light_trusting(
&self,
chain_id: &chain::Id,
commit: &block::Commit,
trust_level: TrustLevelRatio,
) -> Result<()>;
}
impl ValidatorSetExt for Set {
fn verify_commit_light(
&self,
chain_id: &chain::Id,
height: &block::Height,
commit: &block::Commit,
) -> Result<()> {
if self.validators().len() != commit.signatures.len() {
bail_verification!(
"validators signature len ({}) != commit signatures len ({})",
self.validators().len(),
commit.signatures.len(),
)
}
if height != &commit.height {
bail_verification!("height ({}) != commit height ({})", height, commit.height,)
}
let mut tallied_voting_power = 0;
let voting_power_needed =
TrustLevelRatio::new(2, 3).voting_power_needed(self.total_voting_power())?;
for (idx, (validator, commit_sig)) in self
.validators()
.iter()
.zip(commit.signatures.iter())
.enumerate()
{
let signature = match commit_sig {
CommitSig::BlockIdFlagCommit {
signature: Some(sig),
..
} => sig,
CommitSig::BlockIdFlagCommit { .. } => {
bail_verification!("No signature in CommitSig");
}
_ => continue,
};
let vote_sign = commit.vote_sign_bytes(chain_id, idx)?;
validator.verify_signature::<Verifier>(&vote_sign, signature)?;
tallied_voting_power += validator.power();
if tallied_voting_power > voting_power_needed {
return Ok(());
}
}
Err(VerificationError::NotEnoughVotingPower(
tallied_voting_power,
voting_power_needed,
))?
}
fn verify_commit_light_trusting(
&self,
chain_id: &chain::Id,
commit: &block::Commit,
trust_level: TrustLevelRatio,
) -> Result<()> {
let mut seen_vals = HashMap::<usize, usize>::new();
let mut tallied_voting_power = 0;
let voting_power_needed = trust_level.voting_power_needed(self.total_voting_power())?;
for (idx, commit_sig) in commit.signatures.iter().enumerate() {
let (val_id, signature) = match commit_sig {
CommitSig::BlockIdFlagCommit {
validator_address,
signature: Some(sig),
..
} => (validator_address, sig),
CommitSig::BlockIdFlagCommit { .. } => {
bail_verification!("No signature in CommitSig");
}
_ => continue,
};
let Some((val_idx, validator)) = find_validator(self, val_id) else {
continue;
};
if let Some(prev_idx) = seen_vals.get(&val_idx) {
bail_verification!("Double vote from {val_id} ({prev_idx} and {idx}");
}
seen_vals.insert(val_idx, idx);
let vote_sign = commit.vote_sign_bytes(chain_id, idx)?;
validator.verify_signature::<Verifier>(&vote_sign, signature)?;
tallied_voting_power += validator.power();
if tallied_voting_power > voting_power_needed {
return Ok(());
}
}
Err(VerificationError::NotEnoughVotingPower(
tallied_voting_power,
voting_power_needed,
))?
}
}
fn find_validator<'a>(vals: &'a Set, val_id: &account::Id) -> Option<(usize, &'a Info)> {
vals.validators()
.iter()
.enumerate()
.find(|(_idx, val)| val.address == *val_id)
}
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
pub use wbg::*;
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
mod wbg {
use tendermint::validator;
use wasm_bindgen::prelude::*;
use crate::state::auth::JsPublicKey;
#[derive(Clone, Debug)]
#[wasm_bindgen(getter_with_clone, js_name = "ValidatorSet")]
pub struct JsValidatorSet {
pub validators: Vec<JsValidatorInfo>,
pub proposer: Option<JsValidatorInfo>,
pub total_voting_power: u64,
}
impl From<validator::Set> for JsValidatorSet {
fn from(value: validator::Set) -> Self {
JsValidatorSet {
validators: value.validators.into_iter().map(Into::into).collect(),
proposer: value.proposer.map(Into::into),
total_voting_power: value.total_voting_power.value(),
}
}
}
#[derive(Clone, Debug)]
#[wasm_bindgen(getter_with_clone)]
pub struct JsValidatorInfo {
pub address: String,
pub pub_key: JsPublicKey,
pub power: u64,
pub name: Option<String>,
pub proposer_priority: i64,
}
impl From<validator::Info> for JsValidatorInfo {
fn from(value: validator::Info) -> Self {
JsValidatorInfo {
address: value.address.to_string(),
pub_key: value.pub_key.into(),
power: value.power.into(),
name: value.name,
proposer_priority: value.proposer_priority.into(),
}
}
}
}
#[cfg(feature = "uniffi")]
pub mod uniffi_types {
use tendermint::public_key::PublicKey;
use tendermint::validator::Info as TendermintValidatorInfo;
use tendermint::validator::Set as TendermintValidatorSet;
use uniffi::Record;
use crate::error::UniffiConversionError;
use crate::state::UniffiAccountId;
#[derive(Record)]
pub struct ValidatorSet {
pub validators: Vec<ValidatorInfo>,
pub proposer: Option<ValidatorInfo>,
pub total_voting_power: u64,
}
impl From<TendermintValidatorSet> for ValidatorSet {
fn from(value: TendermintValidatorSet) -> Self {
ValidatorSet {
validators: value.validators.into_iter().map(Into::into).collect(),
proposer: value.proposer.map(Into::into),
total_voting_power: value.total_voting_power.value(),
}
}
}
impl TryFrom<ValidatorSet> for TendermintValidatorSet {
type Error = UniffiConversionError;
fn try_from(value: ValidatorSet) -> Result<Self, Self::Error> {
Ok(TendermintValidatorSet {
validators: value
.validators
.into_iter()
.map(|v| v.try_into())
.collect::<Result<Vec<_>, _>>()?,
proposer: value.proposer.map(TryInto::try_into).transpose()?,
total_voting_power: value
.total_voting_power
.try_into()
.map_err(|_| UniffiConversionError::InvalidVotingPower)?,
})
}
}
#[derive(Record)]
pub struct ValidatorInfo {
pub address: UniffiAccountId,
pub pub_key: PublicKey,
pub power: u64,
pub name: Option<String>,
pub proposer_priority: i64,
}
impl From<TendermintValidatorInfo> for ValidatorInfo {
fn from(value: TendermintValidatorInfo) -> Self {
ValidatorInfo {
address: value.address.into(),
pub_key: value.pub_key,
power: value.power.into(),
name: value.name,
proposer_priority: value.proposer_priority.into(),
}
}
}
impl TryFrom<ValidatorInfo> for TendermintValidatorInfo {
type Error = UniffiConversionError;
fn try_from(value: ValidatorInfo) -> Result<Self, Self::Error> {
Ok(TendermintValidatorInfo {
address: value.address.try_into()?,
pub_key: value.pub_key,
power: value
.power
.try_into()
.map_err(|_| UniffiConversionError::InvalidVotingPower)?,
name: value.name,
proposer_priority: value.proposer_priority.into(),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tendermint_proto::v0_38::types::ValidatorSet as RawValidatorSet;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as test;
fn sample_commit() -> block::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_validator_set() -> Set {
serde_json::from_str::<RawValidatorSet>(
r#"{
"validators": [
{
"address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
},
"voting_power": "5000",
"proposer_priority": "0"
}
],
"proposer": {
"address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
},
"voting_power": "5000",
"proposer_priority": "0"
}
}"#,
)
.unwrap()
.try_into()
.unwrap()
}
fn sample_validator_set_no_validators() -> Set {
serde_json::from_str::<RawValidatorSet>(
r#"{
"validators": [],
"proposer": {
"address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
},
"voting_power": "5000",
"proposer_priority": "0"
}
}"#,
)
.unwrap()
.try_into()
.unwrap()
}
fn sample_validator_set_no_proposer() -> Set {
serde_json::from_str::<RawValidatorSet>(
r#"{
"validators": [
{
"address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
"pub_key": {
"type": "tendermint/PubKeyEd25519",
"value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
},
"voting_power": "5000",
"proposer_priority": "0"
}
]
}"#,
)
.unwrap()
.try_into()
.unwrap()
}
#[test]
fn validate_correct() {
sample_validator_set().validate_basic().unwrap();
}
#[test]
fn validate_validators_missing() {
sample_validator_set_no_validators()
.validate_basic()
.unwrap_err();
}
#[test]
fn validate_proposer_missing() {
sample_validator_set_no_proposer()
.validate_basic()
.unwrap_err();
}
#[test]
fn verify_commit_light_success() {
let commit = sample_commit();
let val_set = sample_validator_set();
val_set
.verify_commit_light(
&"private".to_string().try_into().unwrap(),
&1u32.into(),
&commit,
)
.unwrap();
}
#[test]
fn verify_commit_light_validators_and_signatures_mismatch() {
let mut commit = sample_commit();
let val_set = sample_validator_set();
commit.signatures.push(commit.signatures[0].clone());
val_set
.verify_commit_light(
&"private".to_string().try_into().unwrap(),
&1u32.into(),
&commit,
)
.unwrap_err();
}
#[test]
fn verify_commit_light_commit_height_mismatch() {
let mut commit = sample_commit();
let val_set = sample_validator_set();
commit.height = 2u32.into();
val_set
.verify_commit_light(
&"private".to_string().try_into().unwrap(),
&1u32.into(),
&commit,
)
.unwrap_err();
}
}