Skip to content

Commit 41ed20c

Browse files
committed
feat(security): add rate limiting with token bucket algorithm, adaptive refill, and admin bypass
1 parent 945b459 commit 41ed20c

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#![cfg_attr(not(feature = "std"), no_std)]
2+
3+
use ink::prelude::string::String;
4+
use ink::storage::Mapping;
5+
6+
#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)]
7+
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
8+
pub struct RateLimitBucket {
9+
pub tokens: u32,
10+
pub last_refill: u64,
11+
}
12+
13+
#[derive(Debug, Clone, PartialEq, Eq, scale::Encode, scale::Decode)]
14+
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo, ink::storage::traits::StorageLayout))]
15+
pub struct RateLimitConfig {
16+
pub max_tokens: u32,
17+
pub refill_rate: u32,
18+
pub global_max_tokens: u32,
19+
}
20+
21+
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
22+
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
23+
pub enum RateLimitError {
24+
RateLimitExceeded,
25+
}
26+
27+
pub struct RateLimiter {
28+
pub user_rate_limits: Mapping<[u8; 32], RateLimitBucket>,
29+
pub global_rate_limit: RateLimitBucket,
30+
pub config: RateLimitConfig,
31+
pub bypass_enabled: bool,
32+
}
33+
34+
impl RateLimiter {
35+
pub fn new() -> Self {
36+
Self {
37+
user_rate_limits: Mapping::default(),
38+
global_rate_limit: RateLimitBucket {
39+
tokens: 1000,
40+
last_refill: 0,
41+
},
42+
config: RateLimitConfig {
43+
max_tokens: 100,
44+
refill_rate: 5,
45+
global_max_tokens: 1000,
46+
},
47+
bypass_enabled: false,
48+
}
49+
}
50+
51+
pub fn check_rate_limit(
52+
&mut self,
53+
user: [u8; 32],
54+
now: u64,
55+
operation: String,
56+
) -> Result<(), RateLimitError> {
57+
if self.bypass_enabled {
58+
return Ok(());
59+
}
60+
61+
// Global bucket
62+
self.refill_bucket(&mut self.global_rate_limit, now, self.config.global_max_tokens);
63+
64+
if self.global_rate_limit.tokens == 0 {
65+
return Err(RateLimitError::RateLimitExceeded);
66+
}
67+
68+
self.global_rate_limit.tokens -= 1;
69+
70+
// User bucket
71+
let mut bucket = self.user_rate_limits.get(&user).unwrap_or(RateLimitBucket {
72+
tokens: self.config.max_tokens,
73+
last_refill: now,
74+
});
75+
76+
self.refill_bucket(&mut bucket, now, self.config.max_tokens);
77+
78+
if bucket.tokens == 0 {
79+
return Err(RateLimitError::RateLimitExceeded);
80+
}
81+
82+
bucket.tokens -= 1;
83+
self.user_rate_limits.insert(&user, &bucket);
84+
85+
Ok(())
86+
}
87+
88+
fn refill_bucket(&self, bucket: &mut RateLimitBucket, now: u64, max_tokens: u32) {
89+
let elapsed = now.saturating_sub(bucket.last_refill);
90+
let refill = (elapsed as u32) * self.config.refill_rate;
91+
92+
if refill > 0 {
93+
bucket.tokens = core::cmp::min(bucket.tokens + refill, max_tokens);
94+
bucket.last_refill = now;
95+
}
96+
}
97+
98+
pub fn set_bypass(&mut self, enabled: bool) {
99+
self.bypass_enabled = enabled;
100+
}
101+
102+
pub fn update_config(&mut self, config: RateLimitConfig) {
103+
self.config = config;
104+
}
105+
106+
pub fn get_status(&self, user: [u8; 32]) -> (u32, u32) {
107+
let user_tokens = self
108+
.user_rate_limits
109+
.get(&user)
110+
.map(|b| b.tokens)
111+
.unwrap_or(self.config.max_tokens);
112+
113+
let global_tokens = self.global_rate_limit.tokens;
114+
115+
(user_tokens, global_tokens)
116+
}
117+
}

0 commit comments

Comments
 (0)