1use std::collections::HashMap;
2
3use tendermint::block::CommitSig;
4use tendermint::crypto::default::signature::Verifier;
5use tendermint::validator::{Info, Set};
6use tendermint::{account, block, chain};
7
8use crate::block::CommitExt;
9use crate::trust_level::TrustLevelRatio;
10use crate::{
11 bail_validation, bail_verification, Result, ValidateBasic, ValidationError, VerificationError,
12};
13
14impl ValidateBasic for Set {
15 fn validate_basic(&self) -> Result<(), ValidationError> {
16 if self.validators().is_empty() {
17 bail_validation!("validatiors is empty")
18 }
19
20 if self.proposer().is_none() {
21 bail_validation!("proposer is none")
22 }
23
24 Ok(())
25 }
26}
27
28pub trait ValidatorSetExt {
30 fn verify_commit_light(
32 &self,
33 chain_id: &chain::Id,
34 height: &block::Height,
35 commit: &block::Commit,
36 ) -> Result<()>;
37
38 fn verify_commit_light_trusting(
40 &self,
41 chain_id: &chain::Id,
42 commit: &block::Commit,
43 trust_level: TrustLevelRatio,
44 ) -> Result<()>;
45}
46
47impl ValidatorSetExt for Set {
48 fn verify_commit_light(
49 &self,
50 chain_id: &chain::Id,
51 height: &block::Height,
52 commit: &block::Commit,
53 ) -> Result<()> {
54 if self.validators().len() != commit.signatures.len() {
55 bail_verification!(
56 "validators signature len ({}) != commit signatures len ({})",
57 self.validators().len(),
58 commit.signatures.len(),
59 )
60 }
61
62 if height != &commit.height {
63 bail_verification!("height ({}) != commit height ({})", height, commit.height,)
64 }
65
66 let mut tallied_voting_power = 0;
67 let voting_power_needed =
68 TrustLevelRatio::new(2, 3).voting_power_needed(self.total_voting_power())?;
69
70 for (idx, (validator, commit_sig)) in self
71 .validators()
72 .iter()
73 .zip(commit.signatures.iter())
74 .enumerate()
75 {
76 let signature = match commit_sig {
77 CommitSig::BlockIdFlagCommit {
78 signature: Some(ref sig),
79 ..
80 } => sig,
81 CommitSig::BlockIdFlagCommit { .. } => {
82 bail_verification!("No signature in CommitSig");
83 }
84 _ => continue,
86 };
87 let vote_sign = commit.vote_sign_bytes(chain_id, idx)?;
88 validator.verify_signature::<Verifier>(&vote_sign, signature)?;
89
90 tallied_voting_power += validator.power();
91 if tallied_voting_power > voting_power_needed {
92 return Ok(());
93 }
94 }
95
96 Err(VerificationError::NotEnoughVotingPower(
97 tallied_voting_power,
98 voting_power_needed,
99 ))?
100 }
101
102 fn verify_commit_light_trusting(
103 &self,
104 chain_id: &chain::Id,
105 commit: &block::Commit,
106 trust_level: TrustLevelRatio,
107 ) -> Result<()> {
108 let mut seen_vals = HashMap::<usize, usize>::new();
109 let mut tallied_voting_power = 0;
110
111 let voting_power_needed = trust_level.voting_power_needed(self.total_voting_power())?;
112
113 for (idx, commit_sig) in commit.signatures.iter().enumerate() {
114 let (val_id, signature) = match commit_sig {
115 CommitSig::BlockIdFlagCommit {
116 validator_address,
117 signature: Some(ref sig),
118 ..
119 } => (validator_address, sig),
120 CommitSig::BlockIdFlagCommit { .. } => {
121 bail_verification!("No signature in CommitSig");
122 }
123 _ => continue,
125 };
126
127 let Some((val_idx, validator)) = find_validator(self, val_id) else {
128 continue;
129 };
130
131 if let Some(prev_idx) = seen_vals.get(&val_idx) {
132 bail_verification!("Double vote from {val_id} ({prev_idx} and {idx}");
133 }
134
135 seen_vals.insert(val_idx, idx);
136
137 let vote_sign = commit.vote_sign_bytes(chain_id, idx)?;
138 validator.verify_signature::<Verifier>(&vote_sign, signature)?;
139
140 tallied_voting_power += validator.power();
141
142 if tallied_voting_power > voting_power_needed {
143 return Ok(());
144 }
145 }
146
147 Err(VerificationError::NotEnoughVotingPower(
148 tallied_voting_power,
149 voting_power_needed,
150 ))?
151 }
152}
153
154fn find_validator<'a>(vals: &'a Set, val_id: &account::Id) -> Option<(usize, &'a Info)> {
155 vals.validators()
156 .iter()
157 .enumerate()
158 .find(|(_idx, val)| val.address == *val_id)
159}
160
161#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
162pub use wbg::*;
163
164#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
165mod wbg {
166 use tendermint::validator;
167 use wasm_bindgen::prelude::*;
168
169 use crate::state::auth::JsPublicKey;
170
171 #[derive(Clone, Debug)]
173 #[wasm_bindgen(getter_with_clone, js_name = "ValidatorSet")]
174 pub struct JsValidatorSet {
175 pub validators: Vec<JsValidatorInfo>,
177 pub proposer: Option<JsValidatorInfo>,
179 pub total_voting_power: u64,
181 }
182
183 impl From<validator::Set> for JsValidatorSet {
184 fn from(value: validator::Set) -> Self {
185 JsValidatorSet {
186 validators: value.validators.into_iter().map(Into::into).collect(),
187 proposer: value.proposer.map(Into::into),
188 total_voting_power: value.total_voting_power.value(),
189 }
190 }
191 }
192
193 #[derive(Clone, Debug)]
195 #[wasm_bindgen(getter_with_clone)]
196 pub struct JsValidatorInfo {
197 pub address: String,
199 pub pub_key: JsPublicKey,
201 pub power: u64,
203 pub name: Option<String>,
205 pub proposer_priority: i64,
207 }
208
209 impl From<validator::Info> for JsValidatorInfo {
210 fn from(value: validator::Info) -> Self {
211 JsValidatorInfo {
212 address: value.address.to_string(),
213 pub_key: value.pub_key.into(),
214 power: value.power.into(),
215 name: value.name,
216 proposer_priority: value.proposer_priority.into(),
217 }
218 }
219 }
220}
221
222#[cfg(feature = "uniffi")]
224pub mod uniffi_types {
225 use tendermint::public_key::PublicKey;
226 use tendermint::validator::Info as TendermintValidatorInfo;
227 use tendermint::validator::Set as TendermintValidatorSet;
228 use uniffi::Record;
229
230 use crate::error::UniffiConversionError;
231 use crate::state::UniffiAccountId;
232
233 #[derive(Record)]
235 pub struct ValidatorSet {
236 pub validators: Vec<ValidatorInfo>,
238 pub proposer: Option<ValidatorInfo>,
240 pub total_voting_power: u64,
242 }
243
244 impl From<TendermintValidatorSet> for ValidatorSet {
245 fn from(value: TendermintValidatorSet) -> Self {
246 ValidatorSet {
247 validators: value.validators.into_iter().map(Into::into).collect(),
248 proposer: value.proposer.map(Into::into),
249 total_voting_power: value.total_voting_power.value(),
250 }
251 }
252 }
253
254 impl TryFrom<ValidatorSet> for TendermintValidatorSet {
255 type Error = UniffiConversionError;
256
257 fn try_from(value: ValidatorSet) -> Result<Self, Self::Error> {
258 Ok(TendermintValidatorSet {
259 validators: value
260 .validators
261 .into_iter()
262 .map(|v| v.try_into())
263 .collect::<Result<Vec<_>, _>>()?,
264 proposer: value.proposer.map(TryInto::try_into).transpose()?,
265 total_voting_power: value
266 .total_voting_power
267 .try_into()
268 .map_err(|_| UniffiConversionError::InvalidVotingPower)?,
269 })
270 }
271 }
272
273 #[derive(Record)]
275 pub struct ValidatorInfo {
276 pub address: UniffiAccountId,
278 pub pub_key: PublicKey,
280 pub power: u64,
282 pub name: Option<String>,
284 pub proposer_priority: i64,
286 }
287
288 impl From<TendermintValidatorInfo> for ValidatorInfo {
289 fn from(value: TendermintValidatorInfo) -> Self {
290 ValidatorInfo {
291 address: value.address.into(),
292 pub_key: value.pub_key,
293 power: value.power.into(),
294 name: value.name,
295 proposer_priority: value.proposer_priority.into(),
296 }
297 }
298 }
299
300 impl TryFrom<ValidatorInfo> for TendermintValidatorInfo {
301 type Error = UniffiConversionError;
302 fn try_from(value: ValidatorInfo) -> Result<Self, Self::Error> {
303 Ok(TendermintValidatorInfo {
304 address: value.address.try_into()?,
305 pub_key: value.pub_key,
306 power: value
307 .power
308 .try_into()
309 .map_err(|_| UniffiConversionError::InvalidVotingPower)?,
310 name: value.name,
311 proposer_priority: value.proposer_priority.into(),
312 })
313 }
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 use tendermint_proto::v0_34::types::ValidatorSet as RawValidatorSet;
322
323 #[cfg(target_arch = "wasm32")]
324 use wasm_bindgen_test::wasm_bindgen_test as test;
325
326 fn sample_commit() -> block::Commit {
327 serde_json::from_str(r#"{
328 "height": "1",
329 "round": 0,
330 "block_id": {
331 "hash": "17F7D5108753C39714DCA67E6A73CE855C6EA9B0071BBD4FFE5D2EF7F3973BFC",
332 "parts": {
333 "total": 1,
334 "hash": "BEEBB79CDA7D0574B65864D3459FAC7F718B82496BD7FE8B6288BF0A98C8EA22"
335 }
336 },
337 "signatures": [
338 {
339 "block_id_flag": 2,
340 "validator_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
341 "timestamp": "2023-06-23T10:40:48.769228056Z",
342 "signature": "HNn4c02eCt2+nGuBs55L8f3DAz9cgy9psLFuzhtg2XCWnlkt2V43TX2b54hQNi7C0fepBEteA3GC01aJM/JJCg=="
343 }
344 ]
345 }"#).unwrap()
346 }
347
348 fn sample_validator_set() -> Set {
349 serde_json::from_str::<RawValidatorSet>(
350 r#"{
351 "validators": [
352 {
353 "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
354 "pub_key": {
355 "type": "tendermint/PubKeyEd25519",
356 "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
357 },
358 "voting_power": "5000",
359 "proposer_priority": "0"
360 }
361 ],
362 "proposer": {
363 "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
364 "pub_key": {
365 "type": "tendermint/PubKeyEd25519",
366 "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
367 },
368 "voting_power": "5000",
369 "proposer_priority": "0"
370 }
371 }"#,
372 )
373 .unwrap()
374 .try_into()
375 .unwrap()
376 }
377
378 fn sample_validator_set_no_validators() -> Set {
379 serde_json::from_str::<RawValidatorSet>(
380 r#"{
381 "validators": [],
382 "proposer": {
383 "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
384 "pub_key": {
385 "type": "tendermint/PubKeyEd25519",
386 "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
387 },
388 "voting_power": "5000",
389 "proposer_priority": "0"
390 }
391 }"#,
392 )
393 .unwrap()
394 .try_into()
395 .unwrap()
396 }
397
398 fn sample_validator_set_no_proposer() -> Set {
399 serde_json::from_str::<RawValidatorSet>(
400 r#"{
401 "validators": [
402 {
403 "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
404 "pub_key": {
405 "type": "tendermint/PubKeyEd25519",
406 "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
407 },
408 "voting_power": "5000",
409 "proposer_priority": "0"
410 }
411 ]
412 }"#,
413 )
414 .unwrap()
415 .try_into()
416 .unwrap()
417 }
418
419 #[test]
420 fn validate_correct() {
421 sample_validator_set().validate_basic().unwrap();
422 }
423
424 #[test]
425 fn validate_validators_missing() {
426 sample_validator_set_no_validators()
427 .validate_basic()
428 .unwrap_err();
429 }
430
431 #[test]
432 fn validate_proposer_missing() {
433 sample_validator_set_no_proposer()
434 .validate_basic()
435 .unwrap_err();
436 }
437
438 #[test]
439 fn verify_commit_light_success() {
440 let commit = sample_commit();
441 let val_set = sample_validator_set();
442
443 val_set
444 .verify_commit_light(
445 &"private".to_string().try_into().unwrap(),
446 &1u32.into(),
447 &commit,
448 )
449 .unwrap();
450 }
451
452 #[test]
453 fn verify_commit_light_validators_and_signatures_mismatch() {
454 let mut commit = sample_commit();
455 let val_set = sample_validator_set();
456 commit.signatures.push(commit.signatures[0].clone());
457
458 val_set
459 .verify_commit_light(
460 &"private".to_string().try_into().unwrap(),
461 &1u32.into(),
462 &commit,
463 )
464 .unwrap_err();
465 }
466
467 #[test]
468 fn verify_commit_light_commit_height_mismatch() {
469 let mut commit = sample_commit();
470 let val_set = sample_validator_set();
471 commit.height = 2u32.into();
472
473 val_set
474 .verify_commit_light(
475 &"private".to_string().try_into().unwrap(),
476 &1u32.into(),
477 &commit,
478 )
479 .unwrap_err();
480 }
481}