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(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}