hotshot_builder_refactored/
block_size_limits.rs

1use std::sync::atomic::Ordering;
2
3use atomic::Atomic;
4use coarsetime::{Duration, Instant};
5
6#[derive(Debug, Clone, Copy, bytemuck::NoUninit)]
7#[repr(C)]
8pub(crate) struct MutableState {
9    /// Current block size limits
10    pub max_block_size: u64,
11    /// Last time we've incremented the max block size, obtained
12    /// as [`coarsetime::Instant::as_ticks()`]
13    pub last_block_size_increment: u64,
14}
15
16/// Adjustable limits for block size ceiled by maximum block size allowed by the protocol.
17/// We will avoid build blocks over this size limit for performance reasons: computing VID
18/// for bigger blocks could be too costly and lead to API timeouts.
19///
20/// Will be decremented if we fail to respond to `claim_block_header_input` request in time,
21/// and periodically incremented in two cases
22/// - we've served a response to `claim_block_header_input` in time and the block we've served
23///   was truncated because of our current max block size policy.
24/// - we've served a response to `claim_block_header_input` in time and [`Self::increment_period`]
25///   has passed since last time we've incremented the block limits
26#[derive(Debug)]
27pub struct BlockSizeLimits {
28    pub(crate) mutable_state: Atomic<MutableState>,
29    /// Maximum block size as defined by protocol. We'll never increment beyond that
30    pub protocol_max_block_size: u64,
31    /// Period between optimistic increments of the block size
32    pub increment_period: Duration,
33}
34
35impl BlockSizeLimits {
36    /// Never go lower than 10 kilobytes
37    pub const MAX_BLOCK_SIZE_FLOOR: u64 = 10_000;
38    /// When adjusting max block size, it will be decremented or incremented
39    /// by current value / `MAX_BLOCK_SIZE_CHANGE_DIVISOR`
40    pub const MAX_BLOCK_SIZE_CHANGE_DIVISOR: u64 = 10;
41
42    pub fn new(protocol_max_block_size: u64, increment_period: std::time::Duration) -> Self {
43        Self {
44            protocol_max_block_size,
45            increment_period: increment_period.into(),
46            mutable_state: Atomic::new(MutableState {
47                max_block_size: protocol_max_block_size,
48                last_block_size_increment: Instant::now().as_ticks(),
49            }),
50        }
51    }
52
53    pub fn max_block_size(&self) -> u64 {
54        self.mutable_state
55            .load(std::sync::atomic::Ordering::Relaxed)
56            .max_block_size
57    }
58
59    /// If increment period has elapsed or `force` flag is set,
60    /// increment [`Self::max_block_size`] by current value * [`Self::MAX_BLOCK_SIZE_CHANGE_DIVISOR`]
61    /// with [`Self::protocol_max_block_size`] as a ceiling
62    pub fn try_increment_block_size(&self, force: bool) {
63        if force
64            || Instant::now().as_ticks().saturating_sub(
65                self.mutable_state
66                    .load(Ordering::Relaxed)
67                    .last_block_size_increment,
68            ) >= self.increment_period.as_ticks()
69        {
70            self.mutable_state
71                .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |previous| {
72                    let max_block_size = std::cmp::min(
73                        previous.max_block_size
74                            + previous
75                                .max_block_size
76                                .div_ceil(Self::MAX_BLOCK_SIZE_CHANGE_DIVISOR),
77                        self.protocol_max_block_size,
78                    );
79                    let last_block_size_increment = Instant::now().as_ticks();
80                    Some(MutableState {
81                        max_block_size,
82                        last_block_size_increment,
83                    })
84                })
85                .expect("Closure always returns Some");
86        }
87    }
88
89    /// Decrement [`Self::max_block_size`] by current value * [`Self::MAX_BLOCK_SIZE_CHANGE_DIVISOR`]
90    /// with [`Self::MAX_BLOCK_SIZE_FLOOR`] as a floor
91    pub fn decrement_block_size(&self) {
92        self.mutable_state
93            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |previous| {
94                let max_block_size = std::cmp::max(
95                    previous.max_block_size.saturating_sub(
96                        previous
97                            .max_block_size
98                            .div_ceil(Self::MAX_BLOCK_SIZE_CHANGE_DIVISOR),
99                    ),
100                    Self::MAX_BLOCK_SIZE_FLOOR,
101                );
102                Some(MutableState {
103                    max_block_size,
104                    last_block_size_increment: previous.last_block_size_increment,
105                })
106            })
107            .expect("Closure always returns Some");
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use hotshot_builder_shared::testing::constants::{
114        TEST_MAX_BLOCK_SIZE_INCREMENT_PERIOD, TEST_PROTOCOL_MAX_BLOCK_SIZE,
115    };
116    use tracing_test::traced_test;
117
118    use super::*;
119
120    #[test]
121    #[traced_test]
122    fn test_increment_block_size() {
123        let mut block_size_limits = BlockSizeLimits::new(
124            TEST_PROTOCOL_MAX_BLOCK_SIZE,
125            std::time::Duration::from_millis(25),
126        );
127        // Simulate decreased limits
128        block_size_limits.mutable_state = Atomic::new(MutableState {
129            max_block_size: TEST_PROTOCOL_MAX_BLOCK_SIZE / 2,
130            last_block_size_increment: Instant::now().as_ticks(),
131        });
132
133        // Shouldn't increment, increment period hasn't passed yet
134        block_size_limits.try_increment_block_size(false);
135        assert!(block_size_limits.max_block_size() == TEST_PROTOCOL_MAX_BLOCK_SIZE / 2);
136
137        // Should increment, increment period hasn't passed yet, but force flag is set
138        block_size_limits.try_increment_block_size(true);
139        assert!(block_size_limits.max_block_size() > TEST_PROTOCOL_MAX_BLOCK_SIZE / 2);
140        let new_size = block_size_limits.max_block_size();
141
142        std::thread::sleep(std::time::Duration::from_millis(30));
143
144        // Should increment, increment period has passed
145        block_size_limits.try_increment_block_size(false);
146        assert!(block_size_limits.max_block_size() > new_size);
147    }
148
149    #[test]
150    #[traced_test]
151    fn test_decrement_block_size() {
152        let block_size_limits = BlockSizeLimits::new(
153            TEST_PROTOCOL_MAX_BLOCK_SIZE,
154            TEST_MAX_BLOCK_SIZE_INCREMENT_PERIOD,
155        );
156        block_size_limits.decrement_block_size();
157        assert!(block_size_limits.max_block_size() < TEST_PROTOCOL_MAX_BLOCK_SIZE);
158    }
159
160    #[test]
161    #[traced_test]
162    fn test_max_block_size_floor() {
163        let block_size_limits = BlockSizeLimits::new(
164            BlockSizeLimits::MAX_BLOCK_SIZE_FLOOR + 1,
165            TEST_MAX_BLOCK_SIZE_INCREMENT_PERIOD,
166        );
167        block_size_limits.decrement_block_size();
168        assert_eq!(
169            block_size_limits.max_block_size(),
170            BlockSizeLimits::MAX_BLOCK_SIZE_FLOOR
171        );
172    }
173}