celestia_types/
validator_set.rs

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
28/// An extension trait for the [`Set`] to allow additional actions.
29pub trait ValidatorSetExt {
30    /// Verify the commit signatures and the voting power of the commit.
31    fn verify_commit_light(
32        &self,
33        chain_id: &chain::Id,
34        height: &block::Height,
35        commit: &block::Commit,
36    ) -> Result<()>;
37
38    /// Verify the commit signatures and the voting power of the commit optimistically.
39    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                // not commiting for the block
85                _ => 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                // not commiting for the block
124                _ => 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    /// Validator set contains a vector of validators
172    #[derive(Clone, Debug)]
173    #[wasm_bindgen(getter_with_clone, js_name = "ValidatorSet")]
174    pub struct JsValidatorSet {
175        /// Validators in the set
176        pub validators: Vec<JsValidatorInfo>,
177        /// Proposer
178        pub proposer: Option<JsValidatorInfo>,
179        /// Total voting power
180        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    /// Validator information
194    #[derive(Clone, Debug)]
195    #[wasm_bindgen(getter_with_clone)]
196    pub struct JsValidatorInfo {
197        /// Validator account address
198        pub address: String,
199        /// Validator public key
200        pub pub_key: JsPublicKey,
201        /// Validator voting power
202        pub power: u64,
203        /// Validator name
204        pub name: Option<String>,
205        /// Validator proposer priority
206        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/// uniffi types
223#[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    /// Validator set contains a vector of validators
234    #[derive(Record)]
235    pub struct ValidatorSet {
236        /// Validators in the set
237        pub validators: Vec<ValidatorInfo>,
238        /// Proposer
239        pub proposer: Option<ValidatorInfo>,
240        /// Total voting power
241        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    /// Validator information
274    #[derive(Record)]
275    pub struct ValidatorInfo {
276        /// Validator account address
277        pub address: UniffiAccountId,
278        /// Validator public key
279        pub pub_key: PublicKey,
280        /// Validator voting power
281        pub power: u64,
282        /// Validator name
283        pub name: Option<String>,
284        /// Validator proposer priority
285        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}