sui_core/
gasless_rate_limiter.rs1use std::sync::Arc;
5use std::time::Instant;
6
7use parking_lot::Mutex;
8use sui_protocol_config::ProtocolConfig;
9
10pub struct ConsensusGaslessCounter {
14 inner: Mutex<ConsensusWindowInner>,
15}
16
17struct ConsensusWindowInner {
18 window_second: u64,
19 count: u64,
20}
21
22impl Default for ConsensusGaslessCounter {
23 fn default() -> Self {
24 Self {
25 inner: Mutex::new(ConsensusWindowInner {
26 window_second: 0,
27 count: 0,
28 }),
29 }
30 }
31}
32
33impl ConsensusGaslessCounter {
34 pub fn record_commit(&self, commit_timestamp_ms: u64, gasless_count: u64) {
35 let second = commit_timestamp_ms / 1000;
36 let mut inner = self.inner.lock();
37 if second > inner.window_second {
38 inner.window_second = second;
39 inner.count = gasless_count;
40 } else if second == inner.window_second {
41 inner.count += gasless_count;
42 }
43 }
44
45 pub fn current_count(&self) -> u64 {
46 self.inner.lock().count
47 }
48}
49
50struct FixedWindowCounter {
52 count: u64,
53 window_start: Instant,
54}
55
56impl FixedWindowCounter {
57 fn try_acquire(&mut self, max_tps: u64) -> bool {
58 if self.window_start.elapsed().as_secs() >= 1 {
59 self.count = 0;
60 self.window_start = Instant::now();
61 }
62 if self.count < max_tps {
63 self.count += 1;
64 true
65 } else {
66 false
67 }
68 }
69}
70
71#[derive(Clone)]
79pub struct GaslessRateLimiter {
80 consensus_counter: Arc<ConsensusGaslessCounter>,
81 local: Arc<Mutex<FixedWindowCounter>>,
82}
83
84impl GaslessRateLimiter {
85 pub fn new(consensus_counter: Arc<ConsensusGaslessCounter>) -> Self {
86 Self {
87 consensus_counter,
88 local: Arc::new(Mutex::new(FixedWindowCounter {
89 count: 0,
90 window_start: Instant::now(),
91 })),
92 }
93 }
94
95 pub fn try_acquire(&self, config: &ProtocolConfig) -> bool {
96 let Some(max_tps) = config.gasless_max_tps_as_option() else {
97 return true;
98 };
99 if self.consensus_counter.current_count() >= max_tps {
100 return false;
101 }
102 self.local.lock().try_acquire(max_tps)
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use sui_protocol_config::ProtocolVersion;
111
112 fn make_config(max_tps: u64) -> ProtocolConfig {
113 let mut config = ProtocolConfig::get_for_version(
114 ProtocolVersion::MAX,
115 sui_protocol_config::Chain::Unknown,
116 );
117 config.enable_gasless_for_testing();
118 config.set_gasless_max_tps_for_testing(max_tps);
119 config
120 }
121
122 fn make_limiter() -> (Arc<ConsensusGaslessCounter>, GaslessRateLimiter) {
123 let counter = Arc::new(ConsensusGaslessCounter::default());
124 let limiter = GaslessRateLimiter::new(counter.clone());
125 (counter, limiter)
126 }
127
128 #[test]
131 fn test_unset_is_unlimited() {
132 let (_, limiter) = make_limiter();
133 let config = ProtocolConfig::get_for_version(
134 ProtocolVersion::new(117),
135 sui_protocol_config::Chain::Unknown,
136 );
137 for _ in 0..100 {
138 assert!(limiter.try_acquire(&config));
139 }
140 }
141
142 #[test]
143 fn test_zero_max_tps_always_rejects() {
144 let (_, limiter) = make_limiter();
145 let config = make_config(0);
146 assert!(!limiter.try_acquire(&config));
147 }
148
149 #[test]
152 fn test_record_commit_new_window_resets() {
153 let counter = ConsensusGaslessCounter::default();
154 counter.record_commit(1000, 10);
155 assert_eq!(counter.current_count(), 10);
156
157 counter.record_commit(2000, 5);
158 assert_eq!(counter.current_count(), 5);
159 }
160
161 #[test]
162 fn test_record_commit_same_window_accumulates() {
163 let counter = ConsensusGaslessCounter::default();
164 counter.record_commit(1000, 10);
165 counter.record_commit(1500, 7);
166 assert_eq!(counter.current_count(), 17);
167 }
168
169 #[test]
170 fn test_record_commit_past_window_ignored() {
171 let counter = ConsensusGaslessCounter::default();
172 counter.record_commit(2000, 10);
173 counter.record_commit(1000, 99);
174 assert_eq!(counter.current_count(), 10);
175 }
176
177 #[test]
180 fn test_local_counter_prevents_burst() {
181 let (_, limiter) = make_limiter();
182 let config = make_config(5);
183 for _ in 0..5 {
184 assert!(limiter.try_acquire(&config));
185 }
186 assert!(!limiter.try_acquire(&config));
187 }
188
189 #[test]
190 fn test_local_window_resets() {
191 let (_, limiter) = make_limiter();
192 let config = make_config(5);
193 for _ in 0..5 {
194 assert!(limiter.try_acquire(&config));
195 }
196 assert!(!limiter.try_acquire(&config));
197 std::thread::sleep(std::time::Duration::from_secs(1));
198 for _ in 0..5 {
199 assert!(limiter.try_acquire(&config));
200 }
201 assert!(!limiter.try_acquire(&config));
202 }
203
204 #[test]
207 fn test_consensus_blocks_before_local_increment() {
208 let (counter, limiter) = make_limiter();
209 let config = make_config(5);
210 counter.record_commit(1000, 5);
211 assert!(!limiter.try_acquire(&config));
212 counter.record_commit(2000, 0);
213 for _ in 0..5 {
214 assert!(limiter.try_acquire(&config));
215 }
216 }
217
218 #[test]
219 fn test_consensus_rejects_at_capacity() {
220 let (counter, limiter) = make_limiter();
221 counter.record_commit(1000, 5);
222 let config = make_config(5);
223 assert!(!limiter.try_acquire(&config));
224 }
225
226 #[test]
227 fn test_consensus_allows_under_capacity() {
228 let (counter, limiter) = make_limiter();
229 counter.record_commit(1000, 4);
230 let config = make_config(5);
231 assert!(limiter.try_acquire(&config));
232 }
233
234 #[test]
235 fn test_window_resets_after_non_gasless_commit() {
236 let (counter, limiter) = make_limiter();
237 counter.record_commit(1000, 5);
238 let config = make_config(5);
239 assert!(!limiter.try_acquire(&config));
240
241 counter.record_commit(2000, 0);
242 assert!(limiter.try_acquire(&config));
243 }
244}