penumbra_sdk_stake/uptime.rs
1use bitvec::prelude::*;
2
3use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
4use serde::{Deserialize, Serialize};
5
6/// Records information on a validator's uptime.
7///
8/// This structure provides two operations:
9///
10/// - recording that a validator did or did not sign a particular block;
11/// - reporting how many of the last N blocks a validator has missed signing.
12///
13/// Internally, the `Uptime` uses a bit vector as a ring buffer, with a `1` bit
14/// recording that the validator signed the block, and `0` recording that it
15/// didn't. For a new validator, the [`Uptime::new`] method initializes the bit
16/// vector with all `1`s as a grace period to ensure that we don't need to
17/// special-case genesis states, new validators, etc.
18#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(try_from = "pb::Uptime", into = "pb::Uptime")]
20pub struct Uptime {
21 // Note: tracking this means we *could* in principle answer queries by
22 // height, they just might be surprising for new validators (we just report
23 // *failures* to sign, not didn't sign)
24 //
25 // can also possibly extend this impl to support resizing the window when we
26 // get to that
27 as_of_block_height: u64,
28 signatures: BitVec<u8, Lsb0>,
29}
30
31impl std::fmt::Debug for Uptime {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("Uptime").finish()
34 }
35}
36
37impl Uptime {
38 /// Initialize an uptime tracker for a new validator.
39 ///
40 /// This method should not be used for existing validators. Instead,
41 /// deserialize the tracker (that should have been) created when the
42 /// validator was created.
43 pub fn new(initial_block_height: u64, signed_blocks_window_len: usize) -> Self {
44 let signatures = bitvec![u8, Lsb0; 1; signed_blocks_window_len];
45
46 Self {
47 as_of_block_height: initial_block_height,
48 signatures,
49 }
50 }
51
52 /// Mark that the validator signed the block at the given height (`true`),
53 /// or did not sign (`false`).
54 ///
55 /// This method errors only if the provided `height` isn't one greater than
56 /// the current block height. This should not happen in normal use (i.e.,
57 /// it's probably reasonable to `expect` on the error), but the method
58 /// takes an explicit height and checks it to flag misuse and detect bugs.
59 pub fn mark_height_as_signed(&mut self, height: u64, signed: bool) -> anyhow::Result<()> {
60 if height != self.as_of_block_height + 1 {
61 anyhow::bail!(
62 "Last block height was {} but next block height is {}",
63 self.as_of_block_height,
64 height
65 );
66 }
67
68 // Use the bit vector as a ring buffer, overwriting the record for N blocks ago with this one.
69 let index = (height as usize) % self.signatures.len();
70 self.signatures.set(index, signed);
71 self.as_of_block_height = height;
72
73 Ok(())
74 }
75
76 /// Counts the number of missed blocks over the window.
77 pub fn num_missed_blocks(&self) -> usize {
78 self.signatures.iter_zeros().len()
79 }
80
81 /// Enumerates the missed blocks over the window in terms of absolute block height.
82 pub fn missed_blocks(&self) -> impl Iterator<Item = u64> + DoubleEndedIterator + '_ {
83 // The height of the latest block that's been recorded:
84 let current_height = self.as_of_block_height;
85 // The length of the window of blocks being recorded:
86 let window_len = self.signatures.len();
87 // The earliest height of a block that has been recorded:
88 let earliest_height = current_height.saturating_sub(window_len as u64 - 1);
89 // The range of block heights that have been recorded:
90 let all_heights = earliest_height..=current_height;
91 // Filter out the heights that were signed:
92 all_heights.filter_map(move |height| {
93 // Index the bit vector as the ring buffer that it is, and invert the bit corresponding
94 // to this height to find out whether it was missed:
95 let index = (height as usize) % window_len;
96 let signed = self.signatures[index];
97 Some(height).filter(|_| !signed)
98 })
99 }
100
101 /// Returns the block height up to which this tracker has recorded.
102 pub fn as_of_height(&self) -> u64 {
103 self.as_of_block_height
104 }
105
106 /// Returns the size of the window of blocks being recorded.
107 pub fn missed_blocks_window(&self) -> usize {
108 self.signatures.len()
109 }
110}
111
112impl DomainType for Uptime {
113 type Proto = pb::Uptime;
114}
115
116impl From<Uptime> for pb::Uptime {
117 fn from(mut val: Uptime) -> pb::Uptime {
118 // canonicalize any unused data
119 val.signatures.set_uninitialized(true);
120 let window_len = val.signatures.len() as u32;
121 let bitvec = val.signatures.into_vec();
122 pb::Uptime {
123 as_of_block_height: val.as_of_block_height,
124 window_len,
125 bitvec,
126 }
127 }
128}
129
130impl TryFrom<pb::Uptime> for Uptime {
131 type Error = anyhow::Error;
132 fn try_from(msg: pb::Uptime) -> Result<Uptime, Self::Error> {
133 let mut signatures = BitVec::from_vec(msg.bitvec);
134 if signatures.len() < msg.window_len as usize {
135 anyhow::bail!("not enough data in bitvec buffer");
136 }
137 signatures.truncate(msg.window_len as usize);
138 Ok(Uptime {
139 signatures,
140 as_of_block_height: msg.as_of_block_height,
141 })
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 use proptest::prelude::*;
150 use std::collections::VecDeque;
151
152 #[test]
153 fn counts_missed_blocks() {
154 let window = 128;
155 let mut uptime = Uptime::new(0, window);
156
157 // Simulate missing every 4th block for a full window
158 for h in 1..(window + 1) {
159 uptime.mark_height_as_signed(h as u64, h % 4 != 0).unwrap();
160 }
161 assert_eq!(uptime.num_missed_blocks(), window / 4);
162
163 // Now miss no blocks and check that the old data is forgotten
164 for h in (window + 1)..(2 * window + 1) {
165 uptime.mark_height_as_signed(h as u64, true).unwrap();
166 }
167 assert_eq!(uptime.num_missed_blocks(), 0);
168
169 // Finally, check that the sanity-checking works
170 assert!(uptime.mark_height_as_signed(0, true).is_err());
171 }
172
173 /// Basic check that if we miss block 1, we report that we missed block 1.
174 #[test]
175 fn enumerate_missed_first_block() {
176 let window = 128;
177 let mut uptime = Uptime::new(0, window);
178
179 // Mark the first block as missed
180 uptime.mark_height_as_signed(1, false).unwrap();
181 let missed_blocks: Vec<_> = uptime.missed_blocks().collect();
182
183 // Check that exactly the first block is missed
184 assert_eq!(missed_blocks, vec![1]);
185 }
186
187 proptest! {
188 /// Ensure that the `Uptime` struct simulates a fixed size queue of (height, signed) tuples,
189 /// and that the `missed_blocks` iterator returns the correct missed blocks.
190 #[test]
191 fn enumerate_uptime_simulates_bounded_queue(
192 (window_len, signed_blocks) in
193 (1..=16usize).prop_flat_map(move |window_len| {
194 proptest::collection::vec(proptest::bool::ANY, 0..window_len * 2)
195 .prop_map(move |signed_blocks| (window_len, signed_blocks))
196 })
197 ) {
198 // We're going to simulate the `Uptime` struct with a VecDeque of (height, signed)
199 // tuples whose length we will keep bounded by the window length.
200 let mut uptime = Uptime::new(0, window_len);
201 let mut simulated_uptime = VecDeque::new();
202
203 // For each (height, signed) tuple in our generated sequence, mark the height as signed
204 // or not signed.
205 for (height, signed) in signed_blocks.into_iter().enumerate() {
206 // Convert the height to a u64 and add 1 because the `Uptime` struct starts out with
207 // an internal height of 0:
208 let height = height as u64 + 1;
209 // Mark it using the real `Uptime` struct:
210 uptime.mark_height_as_signed(height, signed).unwrap();
211 // Mark it using our simulated `VecDeque`, taking care to keep its length bounded by
212 // the window length:
213 simulated_uptime.push_back((height, signed));
214 if simulated_uptime.len() > window_len {
215 simulated_uptime.pop_front();
216 }
217 }
218
219 // Compare the missed blocks from the real `Uptime` struct with the simulated `VecDeque`:
220 let missed_blocks: Vec<_> = uptime.missed_blocks().collect();
221
222 // Retain only the heights from the simulated `VecDeque` that were not signed:
223 simulated_uptime.retain(|(_, signed)| !signed);
224 let simulated_missed_blocks: Vec<_> =
225 simulated_uptime.into_iter().map(|(height, _)| height).collect();
226
227 prop_assert_eq!(missed_blocks, simulated_missed_blocks);
228 }
229 }
230
231 #[test]
232 fn proto_round_trip() {
233 // make a weird size window
234 let mut uptime = Uptime::new(0, 113);
235 // Fill it with some data
236 for h in 1..300u64 {
237 uptime.mark_height_as_signed(h, h % 13 != 0).unwrap();
238 }
239
240 let bytes = uptime.encode_to_vec();
241 let uptime2 = Uptime::decode(bytes.as_slice()).unwrap();
242 assert_eq!(uptime, uptime2);
243 }
244}