hotmint_consensus/
liveness.rs1use std::collections::HashMap;
2
3use hotmint_types::Height;
4use hotmint_types::validator::{ValidatorId, ValidatorSet};
5
6pub struct LivenessTracker {
13 missed: HashMap<ValidatorId, u64>,
15 total_commits: u64,
17 threshold_num: u64,
20 threshold_den: u64,
21}
22
23impl Default for LivenessTracker {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl LivenessTracker {
30 pub fn new() -> Self {
32 Self {
33 missed: HashMap::new(),
34 total_commits: 0,
35 threshold_num: 50,
36 threshold_den: 100,
37 }
38 }
39
40 pub fn record_commit(&mut self, validator_set: &ValidatorSet, signers: &[bool]) {
46 self.total_commits += 1;
47 for (idx, vi) in validator_set.validators().iter().enumerate() {
48 let signed = signers.get(idx).copied().unwrap_or(false);
49 if !signed {
50 *self.missed.entry(vi.id).or_insert(0) += 1;
51 }
52 }
53 }
54
55 pub fn offline_validators(&self) -> Vec<(ValidatorId, u64, u64)> {
60 if self.total_commits == 0 {
61 return vec![];
62 }
63 let mut result = Vec::new();
64 for (&id, &missed) in &self.missed {
65 if missed * self.threshold_den > self.threshold_num * self.total_commits {
68 result.push((id, missed, self.total_commits));
69 }
70 }
71 result.sort_by_key(|&(id, _, _)| id.0);
72 result
73 }
74
75 pub fn reset(&mut self) {
77 self.missed.clear();
78 self.total_commits = 0;
79 }
80
81 pub fn stats(&self, id: ValidatorId) -> (u64, u64) {
83 let missed = self.missed.get(&id).copied().unwrap_or(0);
84 (missed, self.total_commits)
85 }
86
87 pub fn total_commits(&self) -> u64 {
89 self.total_commits
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct OfflineEvidence {
96 pub validator: ValidatorId,
97 pub missed_commits: u64,
98 pub total_commits: u64,
99 pub evidence_height: Height,
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use hotmint_types::crypto::PublicKey;
107 use hotmint_types::validator::{ValidatorInfo, ValidatorSet};
108
109 fn make_vs(n: usize) -> ValidatorSet {
110 let validators: Vec<ValidatorInfo> = (0..n)
111 .map(|i| ValidatorInfo {
112 id: ValidatorId(i as u64),
113 public_key: PublicKey(vec![i as u8]),
114 power: 1,
115 })
116 .collect();
117 ValidatorSet::new(validators)
118 }
119
120 #[test]
121 fn test_all_sign() {
122 let vs = make_vs(4);
123 let mut tracker = LivenessTracker::new();
124 for _ in 0..10 {
125 tracker.record_commit(&vs, &[true, true, true, true]);
126 }
127 assert!(tracker.offline_validators().is_empty());
128 assert_eq!(tracker.total_commits(), 10);
129 }
130
131 #[test]
132 fn test_one_offline() {
133 let vs = make_vs(4);
134 let mut tracker = LivenessTracker::new();
135 for _ in 0..10 {
137 tracker.record_commit(&vs, &[true, true, true, false]);
138 }
139 let offline = tracker.offline_validators();
140 assert_eq!(offline.len(), 1);
141 assert_eq!(offline[0].0, ValidatorId(3));
142 assert_eq!(offline[0].1, 10); }
144
145 #[test]
146 fn test_threshold_boundary() {
147 let vs = make_vs(4);
148 let mut tracker = LivenessTracker::new();
149 for i in 0..10 {
151 let v2_signs = i % 2 == 0;
152 tracker.record_commit(&vs, &[true, true, v2_signs, true]);
153 }
154 assert!(tracker.offline_validators().is_empty());
155
156 tracker.reset();
158 for i in 0..10 {
159 let v2_signs = i < 4; tracker.record_commit(&vs, &[true, true, v2_signs, true]);
161 }
162 let offline = tracker.offline_validators();
163 assert_eq!(offline.len(), 1);
164 assert_eq!(offline[0].0, ValidatorId(2));
165 }
166
167 #[test]
168 fn test_reset() {
169 let vs = make_vs(4);
170 let mut tracker = LivenessTracker::new();
171 for _ in 0..5 {
172 tracker.record_commit(&vs, &[true, true, true, false]);
173 }
174 assert_eq!(tracker.total_commits(), 5);
175 tracker.reset();
176 assert_eq!(tracker.total_commits(), 0);
177 assert!(tracker.offline_validators().is_empty());
178 }
179}