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(test)]
162mod tests {
163    use super::*;
164
165    use tendermint_proto::v0_34::types::ValidatorSet as RawValidatorSet;
166
167    #[cfg(target_arch = "wasm32")]
168    use wasm_bindgen_test::wasm_bindgen_test as test;
169
170    fn sample_commit() -> block::Commit {
171        serde_json::from_str(r#"{
172          "height": "1",
173          "round": 0,
174          "block_id": {
175            "hash": "17F7D5108753C39714DCA67E6A73CE855C6EA9B0071BBD4FFE5D2EF7F3973BFC",
176            "parts": {
177              "total": 1,
178              "hash": "BEEBB79CDA7D0574B65864D3459FAC7F718B82496BD7FE8B6288BF0A98C8EA22"
179            }
180          },
181          "signatures": [
182            {
183              "block_id_flag": 2,
184              "validator_address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
185              "timestamp": "2023-06-23T10:40:48.769228056Z",
186              "signature": "HNn4c02eCt2+nGuBs55L8f3DAz9cgy9psLFuzhtg2XCWnlkt2V43TX2b54hQNi7C0fepBEteA3GC01aJM/JJCg=="
187            }
188          ]
189        }"#).unwrap()
190    }
191
192    fn sample_validator_set() -> Set {
193        serde_json::from_str::<RawValidatorSet>(
194            r#"{
195              "validators": [
196                {
197                  "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
198                  "pub_key": {
199                    "type": "tendermint/PubKeyEd25519",
200                    "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
201                  },
202                  "voting_power": "5000",
203                  "proposer_priority": "0"
204                }
205              ],
206              "proposer": {
207                "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
208                "pub_key": {
209                  "type": "tendermint/PubKeyEd25519",
210                  "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
211                },
212                "voting_power": "5000",
213                "proposer_priority": "0"
214              }
215            }"#,
216        )
217        .unwrap()
218        .try_into()
219        .unwrap()
220    }
221
222    fn sample_validator_set_no_validators() -> Set {
223        serde_json::from_str::<RawValidatorSet>(
224            r#"{
225              "validators": [],
226              "proposer": {
227                "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
228                "pub_key": {
229                  "type": "tendermint/PubKeyEd25519",
230                  "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
231                },
232                "voting_power": "5000",
233                "proposer_priority": "0"
234              }
235            }"#,
236        )
237        .unwrap()
238        .try_into()
239        .unwrap()
240    }
241
242    fn sample_validator_set_no_proposer() -> Set {
243        serde_json::from_str::<RawValidatorSet>(
244            r#"{
245              "validators": [
246                {
247                  "address": "F1F83230835AA69A1AD6EA68C6D894A4106B8E53",
248                  "pub_key": {
249                    "type": "tendermint/PubKeyEd25519",
250                    "value": "yvrJ+hVxB/nh6sKTG+rrrpzyJgr4bxZ5KXM6VEw3t8w="
251                  },
252                  "voting_power": "5000",
253                  "proposer_priority": "0"
254                }
255              ]
256            }"#,
257        )
258        .unwrap()
259        .try_into()
260        .unwrap()
261    }
262
263    #[test]
264    fn validate_correct() {
265        sample_validator_set().validate_basic().unwrap();
266    }
267
268    #[test]
269    fn validate_validators_missing() {
270        sample_validator_set_no_validators()
271            .validate_basic()
272            .unwrap_err();
273    }
274
275    #[test]
276    fn validate_proposer_missing() {
277        sample_validator_set_no_proposer()
278            .validate_basic()
279            .unwrap_err();
280    }
281
282    #[test]
283    fn verify_commit_light_success() {
284        let commit = sample_commit();
285        let val_set = sample_validator_set();
286
287        val_set
288            .verify_commit_light(
289                &"private".to_string().try_into().unwrap(),
290                &1u32.into(),
291                &commit,
292            )
293            .unwrap();
294    }
295
296    #[test]
297    fn verify_commit_light_validators_and_signatures_mismatch() {
298        let mut commit = sample_commit();
299        let val_set = sample_validator_set();
300        commit.signatures.push(commit.signatures[0].clone());
301
302        val_set
303            .verify_commit_light(
304                &"private".to_string().try_into().unwrap(),
305                &1u32.into(),
306                &commit,
307            )
308            .unwrap_err();
309    }
310
311    #[test]
312    fn verify_commit_light_commit_height_mismatch() {
313        let mut commit = sample_commit();
314        let val_set = sample_validator_set();
315        commit.height = 2u32.into();
316
317        val_set
318            .verify_commit_light(
319                &"private".to_string().try_into().unwrap(),
320                &1u32.into(),
321                &commit,
322            )
323            .unwrap_err();
324    }
325}