penumbra_stake/
uptime.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
use bitvec::prelude::*;

use penumbra_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
use serde::{Deserialize, Serialize};

/// Records information on a validator's uptime.
///
/// This structure provides two operations:
///
/// - recording that a validator did or did not sign a particular block;
/// - reporting how many of the last N blocks a validator has missed signing.
///
/// Internally, the `Uptime` uses a bit vector as a ring buffer, with a `1` bit
/// recording that the validator signed the block, and `0` recording that it
/// didn't.  For a new validator, the [`Uptime::new`] method initializes the bit
/// vector with all `1`s as a grace period to ensure that we don't need to
/// special-case genesis states, new validators, etc.
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(try_from = "pb::Uptime", into = "pb::Uptime")]
pub struct Uptime {
    // Note: tracking this means we *could* in principle answer queries by
    // height, they just might be surprising for new validators (we just report
    // *failures* to sign, not didn't sign)
    //
    // can also possibly extend this impl to support resizing the window when we
    // get to that
    as_of_block_height: u64,
    signatures: BitVec<u8, Lsb0>,
}

impl std::fmt::Debug for Uptime {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Uptime").finish()
    }
}

impl Uptime {
    /// Initialize an uptime tracker for a new validator.
    ///
    /// This method should not be used for existing validators.  Instead,
    /// deserialize the tracker (that should have been) created when the
    /// validator was created.
    pub fn new(initial_block_height: u64, signed_blocks_window_len: usize) -> Self {
        let signatures = bitvec![u8, Lsb0; 1; signed_blocks_window_len];

        Self {
            as_of_block_height: initial_block_height,
            signatures,
        }
    }

    /// Mark that the validator signed the block at the given height (`true`),
    /// or did not sign (`false`).
    ///
    /// This method errors only if the provided `height` isn't one greater than
    /// the current block height.  This should not happen in normal use (i.e.,
    /// it's probably reasonable to `expect` on the error), but the method
    /// takes an explicit height and checks it to flag misuse and detect bugs.
    pub fn mark_height_as_signed(&mut self, height: u64, signed: bool) -> anyhow::Result<()> {
        if height != self.as_of_block_height + 1 {
            anyhow::bail!(
                "Last block height was {} but next block height is {}",
                self.as_of_block_height,
                height
            );
        }

        // Use the bit vector as a ring buffer, overwriting the record for N blocks ago with this one.
        let index = (height as usize) % self.signatures.len();
        self.signatures.set(index, signed);
        self.as_of_block_height = height;

        Ok(())
    }

    /// Counts the number of missed blocks over the window.
    pub fn num_missed_blocks(&self) -> usize {
        self.signatures.iter_zeros().len()
    }

    /// Enumerates the missed blocks over the window in terms of absolute block height.
    pub fn missed_blocks(&self) -> impl Iterator<Item = u64> + DoubleEndedIterator + '_ {
        // The height of the latest block that's been recorded:
        let current_height = self.as_of_block_height;
        // The length of the window of blocks being recorded:
        let window_len = self.signatures.len();
        // The earliest height of a block that has been recorded:
        let earliest_height = current_height.saturating_sub(window_len as u64 - 1);
        // The range of block heights that have been recorded:
        let all_heights = earliest_height..=current_height;
        // Filter out the heights that were signed:
        all_heights.filter_map(move |height| {
            // Index the bit vector as the ring buffer that it is, and invert the bit corresponding
            // to this height to find out whether it was missed:
            let index = (height as usize) % window_len;
            let signed = self.signatures[index];
            Some(height).filter(|_| !signed)
        })
    }

    /// Returns the block height up to which this tracker has recorded.
    pub fn as_of_height(&self) -> u64 {
        self.as_of_block_height
    }

    /// Returns the size of the window of blocks being recorded.
    pub fn missed_blocks_window(&self) -> usize {
        self.signatures.len()
    }
}

impl DomainType for Uptime {
    type Proto = pb::Uptime;
}

impl From<Uptime> for pb::Uptime {
    fn from(mut val: Uptime) -> pb::Uptime {
        // canonicalize any unused data
        val.signatures.set_uninitialized(true);
        let window_len = val.signatures.len() as u32;
        let bitvec = val.signatures.into_vec();
        pb::Uptime {
            as_of_block_height: val.as_of_block_height,
            window_len,
            bitvec,
        }
    }
}

impl TryFrom<pb::Uptime> for Uptime {
    type Error = anyhow::Error;
    fn try_from(msg: pb::Uptime) -> Result<Uptime, Self::Error> {
        let mut signatures = BitVec::from_vec(msg.bitvec);
        if signatures.len() < msg.window_len as usize {
            anyhow::bail!("not enough data in bitvec buffer");
        }
        signatures.truncate(msg.window_len as usize);
        Ok(Uptime {
            signatures,
            as_of_block_height: msg.as_of_block_height,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use proptest::prelude::*;
    use std::collections::VecDeque;

    #[test]
    fn counts_missed_blocks() {
        let window = 128;
        let mut uptime = Uptime::new(0, window);

        // Simulate missing every 4th block for a full window
        for h in 1..(window + 1) {
            uptime.mark_height_as_signed(h as u64, h % 4 != 0).unwrap();
        }
        assert_eq!(uptime.num_missed_blocks(), window / 4);

        // Now miss no blocks and check that the old data is forgotten
        for h in (window + 1)..(2 * window + 1) {
            uptime.mark_height_as_signed(h as u64, true).unwrap();
        }
        assert_eq!(uptime.num_missed_blocks(), 0);

        // Finally, check that the sanity-checking works
        assert!(uptime.mark_height_as_signed(0, true).is_err());
    }

    /// Basic check that if we miss block 1, we report that we missed block 1.
    #[test]
    fn enumerate_missed_first_block() {
        let window = 128;
        let mut uptime = Uptime::new(0, window);

        // Mark the first block as missed
        uptime.mark_height_as_signed(1, false).unwrap();
        let missed_blocks: Vec<_> = uptime.missed_blocks().collect();

        // Check that exactly the first block is missed
        assert_eq!(missed_blocks, vec![1]);
    }

    proptest! {
        /// Ensure that the `Uptime` struct simulates a fixed size queue of (height, signed) tuples,
        /// and that the `missed_blocks` iterator returns the correct missed blocks.
        #[test]
        fn enumerate_uptime_simulates_bounded_queue(
            (window_len, signed_blocks) in
                (1..=16usize).prop_flat_map(move |window_len| {
                    proptest::collection::vec(proptest::bool::ANY, 0..window_len * 2)
                        .prop_map(move |signed_blocks| (window_len, signed_blocks))
                })
        ) {
            // We're going to simulate the `Uptime` struct with a VecDeque of (height, signed)
            // tuples whose length we will keep bounded by the window length.
            let mut uptime = Uptime::new(0, window_len);
            let mut simulated_uptime = VecDeque::new();

            // For each (height, signed) tuple in our generated sequence, mark the height as signed
            // or not signed.
            for (height, signed) in signed_blocks.into_iter().enumerate() {
                // Convert the height to a u64 and add 1 because the `Uptime` struct starts out with
                // an internal height of 0:
                let height = height as u64 + 1;
                // Mark it using the real `Uptime` struct:
                uptime.mark_height_as_signed(height, signed).unwrap();
                // Mark it using our simulated `VecDeque`, taking care to keep its length bounded by
                // the window length:
                simulated_uptime.push_back((height, signed));
                if simulated_uptime.len() > window_len {
                    simulated_uptime.pop_front();
                }
            }

            // Compare the missed blocks from the real `Uptime` struct with the simulated `VecDeque`:
            let missed_blocks: Vec<_> = uptime.missed_blocks().collect();

            // Retain only the heights from the simulated `VecDeque` that were not signed:
            simulated_uptime.retain(|(_, signed)| !signed);
            let simulated_missed_blocks: Vec<_> =
                simulated_uptime.into_iter().map(|(height, _)| height).collect();

            prop_assert_eq!(missed_blocks, simulated_missed_blocks);
        }
    }

    #[test]
    fn proto_round_trip() {
        // make a weird size window
        let mut uptime = Uptime::new(0, 113);
        // Fill it with some data
        for h in 1..300u64 {
            uptime.mark_height_as_signed(h, h % 13 != 0).unwrap();
        }

        let bytes = uptime.encode_to_vec();
        let uptime2 = Uptime::decode(bytes.as_slice()).unwrap();
        assert_eq!(uptime, uptime2);
    }
}