cometbft_light_client_verifier/operations/
voting_power.rs1use alloc::collections::BTreeSet as HashSet;
4use core::{convert::TryFrom, fmt, marker::PhantomData};
5
6use cometbft::{
7 block::CommitSig,
8 crypto::signature,
9 trust_threshold::TrustThreshold as _,
10 vote::{SignedVote, ValidatorIndex, Vote},
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{
15 errors::VerificationError,
16 prelude::*,
17 types::{Commit, SignedHeader, TrustThreshold, ValidatorSet},
18};
19
20#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq)]
22pub struct VotingPowerTally {
23 pub total: u64,
25 pub tallied: u64,
27 pub trust_threshold: TrustThreshold,
29}
30
31impl fmt::Display for VotingPowerTally {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(
34 f,
35 "VotingPower(total={} tallied={} trust_threshold={})",
36 self.total, self.tallied, self.trust_threshold
37 )
38 }
39}
40
41pub trait VotingPowerCalculator: Send + Sync {
45 fn total_power_of(&self, validator_set: &ValidatorSet) -> u64 {
47 validator_set
48 .validators()
49 .iter()
50 .fold(0u64, |total, val_info| total + val_info.power.value())
51 }
52
53 fn check_enough_trust(
56 &self,
57 untrusted_header: &SignedHeader,
58 trusted_validators: &ValidatorSet,
59 trust_threshold: TrustThreshold,
60 ) -> Result<(), VerificationError> {
61 let voting_power =
62 self.voting_power_in(untrusted_header, trusted_validators, trust_threshold)?;
63
64 if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) {
65 Ok(())
66 } else {
67 Err(VerificationError::not_enough_trust(voting_power))
68 }
69 }
70
71 fn check_signers_overlap(
73 &self,
74 untrusted_header: &SignedHeader,
75 untrusted_validators: &ValidatorSet,
76 ) -> Result<(), VerificationError> {
77 let trust_threshold = TrustThreshold::TWO_THIRDS;
78 let voting_power =
79 self.voting_power_in(untrusted_header, untrusted_validators, trust_threshold)?;
80
81 if trust_threshold.is_enough_power(voting_power.tallied, voting_power.total) {
82 Ok(())
83 } else {
84 Err(VerificationError::insufficient_signers_overlap(
85 voting_power,
86 ))
87 }
88 }
89
90 fn voting_power_in(
95 &self,
96 signed_header: &SignedHeader,
97 validator_set: &ValidatorSet,
98 trust_threshold: TrustThreshold,
99 ) -> Result<VotingPowerTally, VerificationError>;
100}
101
102#[derive(Copy, Clone, Debug, PartialEq, Eq)]
105pub struct ProvidedVotingPowerCalculator<V> {
106 _verifier: PhantomData<V>,
107}
108
109unsafe impl<V> Send for ProvidedVotingPowerCalculator<V> {}
111unsafe impl<V> Sync for ProvidedVotingPowerCalculator<V> {}
112
113impl<V> Default for ProvidedVotingPowerCalculator<V> {
114 fn default() -> Self {
115 Self {
116 _verifier: PhantomData,
117 }
118 }
119}
120
121#[cfg(feature = "rust-crypto")]
123pub type ProdVotingPowerCalculator =
124 ProvidedVotingPowerCalculator<cometbft::crypto::default::signature::Verifier>;
125
126impl<V: signature::Verifier> VotingPowerCalculator for ProvidedVotingPowerCalculator<V> {
127 fn voting_power_in(
128 &self,
129 signed_header: &SignedHeader,
130 validator_set: &ValidatorSet,
131 trust_threshold: TrustThreshold,
132 ) -> Result<VotingPowerTally, VerificationError> {
133 let signatures = &signed_header.commit.signatures;
134
135 let mut tallied_voting_power = 0_u64;
136 let mut seen_validators = HashSet::new();
137
138 let non_absent_votes = signatures.iter().enumerate().flat_map(|(idx, signature)| {
140 non_absent_vote(
141 signature,
142 ValidatorIndex::try_from(idx).unwrap(),
143 &signed_header.commit,
144 )
145 .map(|vote| (signature, vote))
146 });
147
148 for (signature, vote) in non_absent_votes {
149 if seen_validators.contains(&vote.validator_address) {
151 return Err(VerificationError::duplicate_validator(
152 vote.validator_address,
153 ));
154 } else {
155 seen_validators.insert(vote.validator_address);
156 }
157
158 let validator = match validator_set.validator(vote.validator_address) {
159 Some(validator) => validator,
160 None => continue, };
162
163 let signed_vote =
164 SignedVote::from_vote(vote.clone(), signed_header.header.chain_id.clone())
165 .ok_or_else(VerificationError::missing_signature)?;
166
167 let sign_bytes = signed_vote.sign_bytes();
169 if validator
170 .verify_signature::<V>(&sign_bytes, signed_vote.signature())
171 .is_err()
172 {
173 return Err(VerificationError::invalid_signature(
174 signed_vote.signature().as_bytes().to_vec(),
175 Box::new(validator),
176 sign_bytes,
177 ));
178 }
179
180 if signature.is_commit() {
182 tallied_voting_power += validator.power();
183 } else {
184 }
187
188 }
191
192 let voting_power = VotingPowerTally {
193 total: self.total_power_of(validator_set),
194 tallied: tallied_voting_power,
195 trust_threshold,
196 };
197
198 Ok(voting_power)
199 }
200}
201
202fn non_absent_vote(
203 commit_sig: &CommitSig,
204 validator_index: ValidatorIndex,
205 commit: &Commit,
206) -> Option<Vote> {
207 let (validator_address, timestamp, signature, block_id) = match commit_sig {
208 CommitSig::BlockIdFlagAbsent { .. } => return None,
209 CommitSig::BlockIdFlagCommit {
210 validator_address,
211 timestamp,
212 signature,
213 } => (
214 *validator_address,
215 *timestamp,
216 signature,
217 Some(commit.block_id),
218 ),
219 CommitSig::BlockIdFlagNil {
220 validator_address,
221 timestamp,
222 signature,
223 } => (*validator_address, *timestamp, signature, None),
224 };
225
226 Some(Vote {
227 vote_type: cometbft::vote::Type::Precommit,
228 height: commit.height,
229 round: commit.round,
230 block_id,
231 timestamp: Some(timestamp),
232 validator_address,
233 validator_index,
234 signature: signature.clone(),
235 extension: Default::default(),
236 extension_signature: None,
237 })
238}
239
240#[cfg(test)]
245mod tests {
246 use cometbft::trust_threshold::TrustThresholdFraction;
247 use cometbft_testgen::{
248 light_block::generate_signed_header, Commit, Generator, Header,
249 LightBlock as TestgenLightBlock, ValidatorSet, Vote as TestgenVote,
250 };
251
252 use super::*;
253 use crate::{errors::VerificationErrorDetail, types::LightBlock};
254
255 const EXPECTED_RESULT: VotingPowerTally = VotingPowerTally {
256 total: 100,
257 tallied: 0,
258 trust_threshold: TrustThresholdFraction::ONE_THIRD,
259 };
260
261 #[test]
262 fn test_empty_signatures() {
263 let vp_calculator = ProdVotingPowerCalculator::default();
264 let trust_threshold = TrustThreshold::default();
265
266 let mut light_block: LightBlock = TestgenLightBlock::new_default(10)
267 .generate()
268 .unwrap()
269 .into();
270 light_block.signed_header.commit.signatures = vec![];
271
272 let result_ok = vp_calculator.voting_power_in(
273 &light_block.signed_header,
274 &light_block.validators,
275 trust_threshold,
276 );
277
278 assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
280 }
281
282 #[test]
283 fn test_all_signatures_absent() {
284 let vp_calculator = ProdVotingPowerCalculator::default();
285 let trust_threshold = TrustThreshold::default();
286
287 let mut testgen_lb = TestgenLightBlock::new_default(10);
288 let mut commit = testgen_lb.commit.clone().unwrap();
289 commit.votes = Some(vec![]);
291 testgen_lb.commit = Some(commit);
292 let light_block: LightBlock = testgen_lb.generate().unwrap().into();
293
294 let result_ok = vp_calculator.voting_power_in(
295 &light_block.signed_header,
296 &light_block.validators,
297 trust_threshold,
298 );
299
300 assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
302 }
303
304 #[test]
305 fn test_all_signatures_nil() {
306 let vp_calculator = ProdVotingPowerCalculator::default();
307 let trust_threshold = TrustThreshold::default();
308
309 let validator_set = ValidatorSet::new(vec!["a", "b"]);
310 let vals = validator_set.clone().validators.unwrap();
311 let header = Header::new(&vals);
312 let votes = vec![
313 TestgenVote::new(vals[0].clone(), header.clone()).nil(true),
314 TestgenVote::new(vals[1].clone(), header.clone()).nil(true),
315 ];
316 let commit = Commit::new_with_votes(header.clone(), 1, votes);
317 let signed_header = generate_signed_header(&header, &commit).unwrap();
318 let valset = validator_set.generate().unwrap();
319
320 let result_ok = vp_calculator.voting_power_in(&signed_header, &valset, trust_threshold);
321
322 assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
324 }
325
326 #[test]
327 fn test_one_invalid_signature() {
328 let vp_calculator = ProdVotingPowerCalculator::default();
329 let trust_threshold = TrustThreshold::default();
330
331 let mut testgen_lb = TestgenLightBlock::new_default(10);
332 let mut commit = testgen_lb.commit.clone().unwrap();
333 let mut votes = commit.votes.unwrap();
334 let vote = votes.pop().unwrap();
335 let header = vote.clone().header.unwrap().chain_id("bad-chain");
336 votes.push(vote.header(header));
337
338 commit.votes = Some(votes);
339 testgen_lb.commit = Some(commit);
340 let light_block: LightBlock = testgen_lb.generate().unwrap().into();
341
342 let result_err = vp_calculator.voting_power_in(
343 &light_block.signed_header,
344 &light_block.validators,
345 trust_threshold,
346 );
347
348 match result_err {
349 Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {},
350 _ => panic!("expected InvalidSignature error"),
351 }
352 }
353
354 #[test]
355 fn test_all_signatures_invalid() {
356 let vp_calculator = ProdVotingPowerCalculator::default();
357 let trust_threshold = TrustThreshold::default();
358
359 let mut testgen_lb = TestgenLightBlock::new_default(10);
360 let header = testgen_lb.header.unwrap().chain_id("bad-chain");
361 testgen_lb.header = Some(header);
362 let light_block: LightBlock = testgen_lb.generate().unwrap().into();
363
364 let result_err = vp_calculator.voting_power_in(
365 &light_block.signed_header,
366 &light_block.validators,
367 trust_threshold,
368 );
369
370 match result_err {
371 Err(VerificationError(VerificationErrorDetail::InvalidSignature(_), _)) => {},
372 _ => panic!("expected InvalidSignature error"),
373 }
374 }
375
376 #[test]
377 fn test_signatures_from_diff_valset() {
378 let vp_calculator = ProdVotingPowerCalculator::default();
379 let trust_threshold = TrustThreshold::default();
380
381 let mut light_block: LightBlock = TestgenLightBlock::new_default(10)
382 .generate()
383 .unwrap()
384 .into();
385 light_block.validators = ValidatorSet::new(vec!["bad-val1", "bad-val2"])
386 .generate()
387 .unwrap();
388
389 let result_ok = vp_calculator.voting_power_in(
390 &light_block.signed_header,
391 &light_block.validators,
392 trust_threshold,
393 );
394
395 assert_eq!(result_ok.unwrap(), EXPECTED_RESULT);
397 }
398}