1use std::collections::{BTreeMap, HashMap, HashSet};
10
11use alloy_primitives::U256;
12use committable::Commitment;
13use hotshot_utils::anytrace::*;
14
15pub use crate::utils::{View, ViewInner};
16use crate::{
17 constants::EPOCH_PARTICIPATION_HISTORY,
18 data::{EpochNumber, Leaf2, VidDisperseShare, ViewNumber},
19 epoch_membership::EpochMembershipCoordinator,
20 message::Proposal,
21 simple_certificate::QuorumCertificate2,
22 simple_vote::HasEpoch,
23 stake_table::{HSStakeTable, StakeTableEntries},
24 traits::{
25 BlockPayload,
26 metrics::{Counter, Gauge, Histogram, Metrics, NoMetrics},
27 node_implementation::NodeType,
28 signature_key::{SignatureKey, StakeTableEntryType},
29 },
30 vote::{Certificate, HasViewNumber},
31};
32
33pub type CommitmentMap<T> = HashMap<Commitment<T>, T>;
35
36pub type VidShares<TYPES> = BTreeMap<
38 ViewNumber,
39 HashMap<
40 <TYPES as NodeType>::SignatureKey,
41 BTreeMap<Option<EpochNumber>, Proposal<TYPES, VidDisperseShare<TYPES>>>,
42 >,
43>;
44
45type ValidatorParticipationMap<TYPES> = HashMap<<TYPES as NodeType>::SignatureKey, (u64, u64)>;
46
47pub struct ParticipationTracker<TYPES: NodeType> {
52 validator: ValidatorParticipation<TYPES>,
53 vote: VoteParticipation<TYPES>,
54}
55
56impl<TYPES: NodeType> Default for ParticipationTracker<TYPES> {
57 fn default() -> Self {
58 let (stake_table, success_threshold) = VoteParticipation::unresolved_stake_table();
59 Self {
60 validator: ValidatorParticipation::new(),
61 vote: VoteParticipation::new(stake_table, success_threshold, EpochNumber::genesis()),
62 }
63 }
64}
65
66impl<TYPES: NodeType> ParticipationTracker<TYPES> {
67 pub fn new(membership: &EpochMembershipCoordinator<TYPES>, epoch: EpochNumber) -> Self {
68 let (stake_table, success_threshold) = resolve_participation_stake_table(membership, epoch);
69 Self {
70 validator: ValidatorParticipation::new_in_epoch(epoch),
71 vote: VoteParticipation::new(stake_table, success_threshold, epoch),
72 }
73 }
74
75 pub fn leader_proposed(&mut self, leader: TYPES::SignatureKey, epoch: EpochNumber) {
76 self.validator.update_participation(leader, epoch, true);
77 }
78
79 pub fn leader_missed(&mut self, leader: TYPES::SignatureKey, epoch: EpochNumber) {
80 self.validator.update_participation(leader, epoch, false);
81 }
82
83 pub fn on_view_changed(&mut self, epoch: EpochNumber) {
84 self.validator.update_participation_epoch(epoch);
85 }
86
87 pub fn on_leaf_decided(
88 &mut self,
89 leaf: &Leaf2<TYPES>,
90 membership: &EpochMembershipCoordinator<TYPES>,
91 ) {
92 if let Err(err) = track_decided_qc_participation(
93 leaf.justify_qc(),
94 membership,
95 &mut self.validator,
96 &mut self.vote,
97 ) {
98 tracing::warn!(%err, "failed to update vote participation epoch");
99 }
100 }
101
102 pub fn current_proposal_participation(&self) -> HashMap<TYPES::SignatureKey, f64> {
103 self.validator.current_proposal_participation()
104 }
105
106 pub fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<TYPES::SignatureKey, f64> {
107 self.validator.proposal_participation(epoch)
108 }
109
110 pub fn current_vote_participation(
111 &self,
112 ) -> HashMap<<TYPES::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
113 self.vote.current_vote_participation()
114 }
115
116 pub fn vote_participation(
117 &self,
118 epoch: EpochNumber,
119 ) -> HashMap<<TYPES::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
120 self.vote.vote_participation(epoch)
121 }
122}
123
124#[derive(Debug, Clone)]
125struct ValidatorParticipation<TYPES: NodeType> {
126 epoch: EpochNumber,
127 current_epoch_participation: ValidatorParticipationMap<TYPES>,
129
130 previous_epoch_participation: BTreeMap<EpochNumber, ValidatorParticipationMap<TYPES>>,
132}
133
134impl<TYPES: NodeType> ValidatorParticipation<TYPES> {
135 fn new() -> Self {
136 Self::new_in_epoch(EpochNumber::genesis())
137 }
138
139 fn new_in_epoch(epoch: EpochNumber) -> Self {
140 Self {
141 epoch,
142 current_epoch_participation: HashMap::new(),
143 previous_epoch_participation: BTreeMap::new(),
144 }
145 }
146
147 fn update_participation(
148 &mut self,
149 key: TYPES::SignatureKey,
150 epoch: EpochNumber,
151 proposed: bool,
152 ) {
153 let participation = match epoch.cmp(&self.epoch) {
154 std::cmp::Ordering::Greater => {
155 self.update_participation_epoch(epoch);
156 &mut self.current_epoch_participation
157 },
158 std::cmp::Ordering::Equal => &mut self.current_epoch_participation,
159 std::cmp::Ordering::Less => {
160 let Some(archived) = self.previous_epoch_participation.get_mut(&epoch) else {
161 return;
162 };
163 archived
164 },
165 };
166 let entry = participation.entry(key).or_insert((0, 0));
167 if proposed {
168 entry.1 += 1;
169 }
170 entry.0 += 1;
171 }
172
173 fn update_participation_epoch(&mut self, epoch: EpochNumber) {
174 if epoch <= self.epoch {
175 return;
176 }
177 self.previous_epoch_participation
178 .insert(self.epoch, self.current_epoch_participation.clone());
179
180 self.previous_epoch_participation =
181 self.previous_epoch_participation
182 .split_off(&EpochNumber::new(
183 self.epoch.saturating_sub(EPOCH_PARTICIPATION_HISTORY),
184 ));
185
186 self.epoch = epoch;
187 self.current_epoch_participation = HashMap::new();
188 }
189
190 fn current_proposal_participation(&self) -> HashMap<TYPES::SignatureKey, f64> {
191 self.current_epoch_participation
192 .iter()
193 .map(|(key, (leader, proposed))| {
194 (
195 key.clone(),
196 if *leader == 0 {
197 0.0
198 } else {
199 *proposed as f64 / *leader as f64
200 },
201 )
202 })
203 .collect()
204 }
205 fn proposal_participation(&self, epoch: EpochNumber) -> HashMap<TYPES::SignatureKey, f64> {
206 let tracked_participation = if epoch == self.epoch {
207 self.current_epoch_participation.clone()
208 } else {
209 self.previous_epoch_participation
210 .get(&epoch)
211 .unwrap_or(&HashMap::new())
212 .clone()
213 };
214
215 tracked_participation
216 .iter()
217 .map(|(key, (leader, proposed))| {
218 (
219 key.clone(),
220 if *leader == 0 {
221 0.0
222 } else {
223 *proposed as f64 / *leader as f64
224 },
225 )
226 })
227 .collect()
228 }
229
230 fn current_epoch(&self) -> EpochNumber {
231 self.epoch
232 }
233}
234
235type VoteParticipationMap<TYPES> = (
236 HashMap<<<TYPES as NodeType>::SignatureKey as SignatureKey>::VerificationKeyType, u64>,
237 u64,
238);
239
240#[derive(Clone, Debug)]
241struct VoteParticipation<TYPES: NodeType> {
242 epoch: EpochNumber,
244
245 stake_table: HSStakeTable<TYPES>,
247
248 success_threshold: U256,
250
251 view_set: HashSet<ViewNumber>,
253
254 current_epoch_num_views: u64,
256
257 current_epoch_participation:
259 HashMap<<TYPES::SignatureKey as SignatureKey>::VerificationKeyType, u64>,
260
261 previous_epoch_participation: BTreeMap<EpochNumber, VoteParticipationMap<TYPES>>,
263}
264
265impl<TYPES: NodeType> VoteParticipation<TYPES> {
266 fn new(stake_table: HSStakeTable<TYPES>, success_threshold: U256, epoch: EpochNumber) -> Self {
267 let current_epoch_participation: HashMap<_, _> = stake_table
268 .iter()
269 .map({
270 |peer_config| {
271 (
272 peer_config
273 .stake_table_entry
274 .public_key()
275 .to_verification_key(),
276 0u64,
277 )
278 }
279 })
280 .collect();
281 Self {
282 epoch,
283 stake_table,
284 success_threshold,
285 view_set: HashSet::new(),
286 current_epoch_num_views: 0u64,
287 current_epoch_participation,
288 previous_epoch_participation: BTreeMap::new(),
289 }
290 }
291
292 fn unresolved_stake_table() -> (HSStakeTable<TYPES>, U256) {
293 (HSStakeTable::default(), U256::MAX)
294 }
295
296 fn stake_table_unresolved(&self) -> bool {
297 self.stake_table.is_empty()
298 }
299
300 fn reseed_stake_table(&mut self, stake_table: HSStakeTable<TYPES>, threshold: U256) {
301 self.current_epoch_participation = stake_table
302 .iter()
303 .map(|peer_config| {
304 (
305 peer_config
306 .stake_table_entry
307 .public_key()
308 .to_verification_key(),
309 0u64,
310 )
311 })
312 .collect();
313 self.stake_table = stake_table;
314 self.success_threshold = threshold;
315 }
316
317 fn update_participation(&mut self, qc: QuorumCertificate2<TYPES>) -> Result<()> {
318 ensure!(
319 qc.epoch() == Some(self.epoch),
320 info!(
321 "Incorrect epoch while updating vote participation, current epoch: {:?}, QC epoch \
322 {:?}",
323 self.epoch,
324 qc.epoch()
325 )
326 );
327 ensure!(
328 !self.view_set.contains(&qc.view_number()),
329 info!(
330 "Participation for view {} already updated",
331 qc.view_number()
332 )
333 );
334 let signers = qc
335 .signers(
336 &StakeTableEntries::<TYPES>::from(self.stake_table.clone()).0,
337 self.success_threshold,
338 )
339 .context(|e| warn!("Tracing signers: {e}"))?;
340 for vk in signers {
341 let Some(votes) = self.current_epoch_participation.get_mut(&vk) else {
342 bail!(warn!(
343 "Trying to update vote participation for unknown key: {:?}",
344 vk
345 ));
346 };
347 *votes += 1;
348 }
349 self.view_set.insert(qc.view_number());
350 self.current_epoch_num_views += 1;
351 Ok(())
352 }
353
354 fn update_participation_epoch(
355 &mut self,
356 stake_table: HSStakeTable<TYPES>,
357 success_threshold: U256,
358 epoch: EpochNumber,
359 ) -> Result<()> {
360 ensure!(
361 epoch >= self.epoch,
362 warn!(
363 "New epoch less than current epoch while updating vote participation epoch, \
364 current epoch: {:?}, new epoch {:?}",
365 self.epoch, epoch
366 )
367 );
368 if epoch == self.epoch {
370 return Ok(());
371 }
372
373 self.previous_epoch_participation.insert(
374 self.epoch,
375 (
376 self.current_epoch_participation.clone(),
377 self.current_epoch_num_views,
378 ),
379 );
380
381 self.previous_epoch_participation =
382 self.previous_epoch_participation
383 .split_off(&EpochNumber::new(
384 self.epoch.saturating_sub(EPOCH_PARTICIPATION_HISTORY),
385 ));
386
387 self.epoch = epoch;
388 self.current_epoch_num_views = 0;
389 self.view_set = HashSet::new();
390 let current_epoch_participation: HashMap<_, _> = stake_table
391 .iter()
392 .map({
393 |peer_config| {
394 (
395 peer_config
396 .stake_table_entry
397 .public_key()
398 .to_verification_key(),
399 0u64,
400 )
401 }
402 })
403 .collect();
404 self.current_epoch_participation = current_epoch_participation;
405 self.stake_table = stake_table;
406 self.success_threshold = success_threshold;
407 Ok(())
408 }
409
410 fn current_vote_participation(
411 &self,
412 ) -> HashMap<<TYPES::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
413 self.current_epoch_participation
414 .iter()
415 .map(|(key, votes)| {
416 (
417 key.clone(),
418 Self::calculate_ratio(votes, self.current_epoch_num_views),
419 )
420 })
421 .collect()
422 }
423 fn vote_participation(
424 &self,
425 epoch: EpochNumber,
426 ) -> HashMap<<TYPES::SignatureKey as SignatureKey>::VerificationKeyType, f64> {
427 let (participation, num_views) = if epoch == self.epoch {
428 (
429 self.current_epoch_participation.clone(),
430 self.current_epoch_num_views,
431 )
432 } else {
433 self.previous_epoch_participation
434 .get(&epoch)
435 .unwrap_or(&(HashMap::new(), 0))
436 .clone()
437 };
438
439 if num_views == 0 {
440 return HashMap::new();
441 }
442
443 participation
444 .iter()
445 .map(|(key, votes)| (key.clone(), Self::calculate_ratio(votes, num_views)))
446 .collect()
447 }
448
449 fn calculate_ratio(num_votes: &u64, total_views: u64) -> f64 {
450 if total_views == 0 {
451 0.0
452 } else {
453 *num_votes as f64 / total_views as f64
454 }
455 }
456
457 fn current_epoch(&self) -> EpochNumber {
458 self.epoch
459 }
460}
461
462fn resolve_participation_stake_table<TYPES: NodeType>(
463 membership: &EpochMembershipCoordinator<TYPES>,
464 epoch: EpochNumber,
465) -> (HSStakeTable<TYPES>, U256) {
466 match membership.stake_table_for_epoch(Some(epoch)) {
467 Ok(m) => (
468 HSStakeTable::from_iter(m.stake_table()),
469 m.success_threshold(),
470 ),
471 Err(err) => {
472 tracing::warn!(?epoch, %err, "no stake table for participation tracking");
473 VoteParticipation::unresolved_stake_table()
474 },
475 }
476}
477
478fn track_decided_qc_participation<TYPES: NodeType>(
479 qc: &QuorumCertificate2<TYPES>,
480 membership: &EpochMembershipCoordinator<TYPES>,
481 validator: &mut ValidatorParticipation<TYPES>,
482 vote: &mut VoteParticipation<TYPES>,
483) -> Result<()> {
484 let Some(qc_epoch) = qc.epoch() else {
485 bail!(warn!(
486 "Decided QC for view {} has no epoch",
487 qc.view_number()
488 ));
489 };
490 if qc_epoch > validator.current_epoch() {
491 validator.update_participation_epoch(qc_epoch);
492 }
493 if qc_epoch > vote.current_epoch() {
494 let (stake_table, success_threshold) =
495 resolve_participation_stake_table(membership, qc_epoch);
496 vote.update_participation_epoch(stake_table, success_threshold, qc_epoch)
497 .context(warn!("Updating vote participation"))?;
498 } else if qc_epoch == vote.current_epoch()
499 && vote.stake_table_unresolved()
500 && let Ok(m) = membership.stake_table_for_epoch(Some(qc_epoch))
501 {
502 vote.reseed_stake_table(
503 HSStakeTable::from_iter(m.stake_table()),
504 m.success_threshold(),
505 );
506 }
507 if let Err(e) = vote.update_participation(qc.clone()) {
508 tracing::warn!("Failed to update vote participation: {e}");
509 }
510 Ok(())
511}
512
513#[derive(Debug, Clone, Hash, Eq, PartialEq)]
515pub struct PayloadWithMetadata<TYPES: NodeType> {
516 pub payload: TYPES::BlockPayload,
517 pub metadata: <TYPES::BlockPayload as BlockPayload<TYPES>>::Metadata,
518}
519
520#[derive(Clone, Debug)]
522pub struct ConsensusMetricsValue {
523 pub last_synced_block_height: Box<dyn Gauge>,
525 pub last_decided_view: Box<dyn Gauge>,
527 pub last_voted_view: Box<dyn Gauge>,
529 pub last_decided_time: Box<dyn Gauge>,
531 pub current_view: Box<dyn Gauge>,
533 pub number_of_views_since_last_decide: Box<dyn Gauge>,
535 pub number_of_views_per_decide_event: Box<dyn Histogram>,
537 pub view_duration_as_leader: Box<dyn Histogram>,
539 pub invalid_qc: Box<dyn Gauge>,
541 pub outstanding_transactions: Box<dyn Gauge>,
543 pub outstanding_transactions_memory_size: Box<dyn Gauge>,
545 pub number_of_timeouts: Box<dyn Counter>,
547 pub number_of_timeouts_as_leader: Box<dyn Counter>,
549 pub number_of_empty_blocks_proposed: Box<dyn Counter>,
551 pub internal_event_queue_len: Box<dyn Gauge>,
553 pub proposal_to_decide_time: Box<dyn Histogram>,
555 pub previous_proposal_to_proposal_time: Box<dyn Histogram>,
557 pub finalized_bytes: Box<dyn Histogram>,
559 pub validate_and_apply_header_duration: Box<dyn Histogram>,
561 pub update_leaf_duration: Box<dyn Histogram>,
563 pub block_commitments_duration: Box<dyn Histogram>,
566 pub vid_disperse_duration: Box<dyn Histogram>,
569}
570
571impl ConsensusMetricsValue {
572 #[must_use]
574 pub fn new(metrics: &dyn Metrics) -> Self {
575 Self {
576 last_synced_block_height: metrics
577 .create_gauge(String::from("last_synced_block_height"), None),
578 last_decided_view: metrics.create_gauge(String::from("last_decided_view"), None),
579 last_voted_view: metrics.create_gauge(String::from("last_voted_view"), None),
580 last_decided_time: metrics.create_gauge(String::from("last_decided_time"), None),
581 current_view: metrics.create_gauge(String::from("current_view"), None),
582 number_of_views_since_last_decide: metrics
583 .create_gauge(String::from("number_of_views_since_last_decide"), None),
584 number_of_views_per_decide_event: metrics
585 .create_histogram(String::from("number_of_views_per_decide_event"), None),
586 view_duration_as_leader: metrics
587 .create_histogram(String::from("view_duration_as_leader"), None),
588 invalid_qc: metrics.create_gauge(String::from("invalid_qc"), None),
589 outstanding_transactions: metrics
590 .create_gauge(String::from("outstanding_transactions"), None),
591 outstanding_transactions_memory_size: metrics
592 .create_gauge(String::from("outstanding_transactions_memory_size"), None),
593 number_of_timeouts: metrics.create_counter(String::from("number_of_timeouts"), None),
594 number_of_timeouts_as_leader: metrics
595 .create_counter(String::from("number_of_timeouts_as_leader"), None),
596 number_of_empty_blocks_proposed: metrics
597 .create_counter(String::from("number_of_empty_blocks_proposed"), None),
598 internal_event_queue_len: metrics
599 .create_gauge(String::from("internal_event_queue_len"), None),
600 proposal_to_decide_time: metrics
601 .create_histogram(String::from("proposal_to_decide_time"), None),
602 previous_proposal_to_proposal_time: metrics
603 .create_histogram(String::from("previous_proposal_to_proposal_time"), None),
604 finalized_bytes: metrics.create_histogram(String::from("finalized_bytes"), None),
605 validate_and_apply_header_duration: metrics.create_histogram(
606 String::from("validate_and_apply_header_duration"),
607 Some("seconds".to_string()),
608 ),
609 update_leaf_duration: metrics.create_histogram(
610 String::from("update_leaf_duration"),
611 Some("seconds".to_string()),
612 ),
613 block_commitments_duration: metrics.create_histogram(
614 String::from("block_commitments_duration"),
615 Some("seconds".to_string()),
616 ),
617 vid_disperse_duration: metrics.create_histogram(
618 String::from("vid_disperse_duration"),
619 Some("seconds".to_string()),
620 ),
621 }
622 }
623}
624
625impl Default for ConsensusMetricsValue {
626 fn default() -> Self {
627 Self::new(&*NoMetrics::boxed())
628 }
629}