Skip to content

Commit cadf9ca

Browse files
authored
Merge pull request #191 from Josue19-08/feat/rate-limiting
Implement Rate Limiting
2 parents 1a6af6b + f73d19c commit cadf9ca

21 files changed

+810
-13
lines changed

contracts/course/course_registry/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub enum Error {
5555
InvalidPrice100 = 54,
5656
AlreadyInitialized = 55,
5757
DuplicatePrerequisite = 56,
58+
// Rate limiting errors
59+
CourseRateLimitExceeded = 57,
60+
CourseRateLimitNotConfigured = 58,
5861
}
5962

6063
pub fn handle_error(env: &Env, error: Error) -> ! {

contracts/course/course_registry/src/functions/access_control.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use soroban_sdk::{symbol_short, Address, Env, String, Symbol, IntoVal};
55

66
use crate::error::{handle_error, Error};
77
use crate::schema::Course;
8+
use super::course_rate_limit_utils::initialize_course_rate_limit_config;
89

910
const COURSE_KEY: Symbol = symbol_short!("course");
1011

@@ -67,6 +68,10 @@ pub fn initialize(env: &Env, owner: &Address, user_mgmt_addr: &Address) {
6768
env.storage()
6869
.instance()
6970
.set(&(KEY_USER_MGMT_ADDR,), user_mgmt_addr);
71+
72+
// Initialize rate limiting configuration
73+
initialize_course_rate_limit_config(env);
74+
7075
env.events()
7176
.publish((INIT_ACCESS_CONTROL_EVENT,), (owner, user_mgmt_addr));
7277
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) 2025 SkillCert
3+
4+
use crate::error::{handle_error, Error};
5+
use crate::schema::{DataKey, CourseRateLimitData, CourseRateLimitConfig, DEFAULT_COURSE_RATE_LIMIT_WINDOW, DEFAULT_MAX_COURSE_CREATIONS_PER_WINDOW};
6+
use soroban_sdk::{Address, Env};
7+
8+
/// Check if the user has exceeded the rate limit for course creation operations.
9+
///
10+
/// This function validates if the caller can perform a course creation operation
11+
/// based on the configured rate limiting rules.
12+
///
13+
/// # Arguments
14+
/// * `env` - The Soroban environment
15+
/// * `creator` - The address attempting to create a course
16+
///
17+
/// # Panics
18+
/// * If rate limit is exceeded
19+
/// * If rate limit configuration is not found
20+
pub fn check_course_creation_rate_limit(env: &Env, creator: &Address) {
21+
// Get rate limit configuration
22+
let config_key = DataKey::CourseRateLimitConfig;
23+
let rate_config = match env
24+
.storage()
25+
.persistent()
26+
.get::<DataKey, CourseRateLimitConfig>(&config_key)
27+
{
28+
Some(config) => config,
29+
None => {
30+
// If no configuration exists, use default
31+
get_default_course_rate_limit_config()
32+
}
33+
};
34+
35+
let current_time = env.ledger().timestamp();
36+
let rate_limit_key = DataKey::CourseRateLimit(creator.clone());
37+
38+
// Get existing rate limit data or create new one
39+
let mut rate_data = match env
40+
.storage()
41+
.persistent()
42+
.get::<DataKey, CourseRateLimitData>(&rate_limit_key)
43+
{
44+
Some(data) => data,
45+
None => CourseRateLimitData {
46+
count: 0,
47+
window_start: current_time,
48+
}
49+
};
50+
51+
// Check if we need to reset the window
52+
if current_time >= rate_data.window_start + rate_config.window_seconds {
53+
// Reset the window
54+
rate_data.count = 0;
55+
rate_data.window_start = current_time;
56+
}
57+
58+
// Check if user has exceeded the rate limit
59+
if rate_data.count >= rate_config.max_courses_per_window {
60+
handle_error(env, Error::CourseRateLimitExceeded);
61+
}
62+
63+
// Increment the count and save
64+
rate_data.count += 1;
65+
env.storage()
66+
.persistent()
67+
.set(&rate_limit_key, &rate_data);
68+
}
69+
70+
/// Get the default rate limiting configuration for course operations.
71+
///
72+
/// This function returns the default rate limiting settings that can be
73+
/// used when initializing the system or when no custom configuration is set.
74+
pub fn get_default_course_rate_limit_config() -> CourseRateLimitConfig {
75+
CourseRateLimitConfig {
76+
window_seconds: DEFAULT_COURSE_RATE_LIMIT_WINDOW,
77+
max_courses_per_window: DEFAULT_MAX_COURSE_CREATIONS_PER_WINDOW,
78+
}
79+
}
80+
81+
/// Initialize the default rate limiting configuration for course operations.
82+
///
83+
/// This function should be called during system initialization to set up
84+
/// the default rate limiting configuration.
85+
///
86+
/// # Arguments
87+
/// * `env` - The Soroban environment
88+
pub fn initialize_course_rate_limit_config(env: &Env) {
89+
let config_key = DataKey::CourseRateLimitConfig;
90+
91+
// Only initialize if not already set
92+
if !env.storage().persistent().has(&config_key) {
93+
let default_config = get_default_course_rate_limit_config();
94+
env.storage()
95+
.persistent()
96+
.set(&config_key, &default_config);
97+
}
98+
}
99+
100+
/// Update the course rate limiting configuration.
101+
///
102+
/// This function allows administrators to modify the rate limiting settings.
103+
///
104+
/// # Arguments
105+
/// * `env` - The Soroban environment
106+
/// * `new_config` - The new rate limiting configuration
107+
pub fn update_course_rate_limit_config(env: &Env, new_config: CourseRateLimitConfig) {
108+
let config_key = DataKey::CourseRateLimitConfig;
109+
env.storage()
110+
.persistent()
111+
.set(&config_key, &new_config);
112+
}
113+

contracts/course/course_registry/src/functions/create_course.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// SPDX-License-Identifier: MIT
22
// Copyright (c) 2025 SkillCert
33

4+
use super::utils::{to_lowercase, trim, u32_to_string};
5+
use super::course_rate_limit_utils::check_course_creation_rate_limit;
46
use soroban_sdk::{symbol_short, Address, Env, String, Symbol, Vec};
5-
67
use crate::error::{handle_error, Error};
78
use crate::schema::{Course, CourseLevel};
8-
use crate::functions::utils::{to_lowercase, trim, u32_to_string};
99

1010
const COURSE_KEY: Symbol = symbol_short!("course");
1111
const TITLE_KEY: Symbol = symbol_short!("title");
@@ -28,6 +28,9 @@ pub fn create_course(
2828
) -> Course {
2929
creator.require_auth();
3030

31+
// Check rate limiting before proceeding with course creation
32+
check_course_creation_rate_limit(&env, &creator);
33+
3134
// ensure the title is not empty and not just whitespace
3235
let trimmed_title: String = trim(&env, &title);
3336
if title.is_empty() || trimmed_title.is_empty() {

contracts/course/course_registry/src/functions/edit_prerequisite.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,17 @@ mod tests {
221221

222222
let contract_id = env.register(CourseRegistry, ());
223223
let client = CourseRegistryClient::new(&env, &contract_id);
224+
225+
// Initialize course rate limiting with permissive settings for testing
226+
env.as_contract(&contract_id, || {
227+
use crate::schema::{DataKey, CourseRateLimitConfig};
228+
let permissive_config = CourseRateLimitConfig {
229+
window_seconds: 3600,
230+
max_courses_per_window: 100,
231+
};
232+
let config_key = DataKey::CourseRateLimitConfig;
233+
env.storage().persistent().set(&config_key, &permissive_config);
234+
});
224235

225236
let creator: Address = Address::generate(&env);
226237
let course1 = client.create_course(
@@ -521,6 +532,17 @@ mod tests {
521532

522533
let contract_id = env.register(CourseRegistry, ());
523534
let client = CourseRegistryClient::new(&env, &contract_id);
535+
536+
// Initialize course rate limiting with permissive settings for testing
537+
env.as_contract(&contract_id, || {
538+
use crate::schema::{DataKey, CourseRateLimitConfig};
539+
let permissive_config = CourseRateLimitConfig {
540+
window_seconds: 3600,
541+
max_courses_per_window: 100,
542+
};
543+
let config_key = DataKey::CourseRateLimitConfig;
544+
env.storage().persistent().set(&config_key, &permissive_config);
545+
});
524546

525547
let creator: Address = Address::generate(&env);
526548
let course1 = client.create_course(

contracts/course/course_registry/src/functions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod contract_versioning;
99
pub mod create_course;
1010
pub mod create_course_category;
1111
pub mod create_prerequisite;
12+
pub mod course_rate_limit_utils;
1213
pub mod delete_course;
1314
pub mod edit_course;
1415
pub mod edit_goal;

contracts/course/course_registry/src/schema.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ pub const FILTER_MIN_PRICE: u128 = 500;
1010
pub const MAX_SCAN_ID: u32 = 50;
1111
pub const MAX_EMPTY_CHECKS: u32 = 10;
1212

13+
/// Rate limiting constants for course operations
14+
pub const DEFAULT_COURSE_RATE_LIMIT_WINDOW: u64 = 3600; // 1 hour in seconds
15+
pub const DEFAULT_MAX_COURSE_CREATIONS_PER_WINDOW: u32 = 3; // Max course creations per hour per address
16+
1317
#[contracttype]
1418
#[derive(Clone, Debug, PartialEq)]
1519
pub struct CourseModule {
@@ -30,6 +34,30 @@ pub struct CourseGoal {
3034
pub created_at: u64,
3135
}
3236

37+
/// Rate limiting configuration for course operations.
38+
///
39+
/// Tracks rate limiting settings for spam protection in course creation.
40+
#[contracttype]
41+
#[derive(Clone, Debug, PartialEq)]
42+
pub struct CourseRateLimitConfig {
43+
/// Time window for rate limiting in seconds
44+
pub window_seconds: u64,
45+
/// Maximum course creations allowed per window
46+
pub max_courses_per_window: u32,
47+
}
48+
49+
/// Rate limiting tracking data for course operations per address.
50+
///
51+
/// Stores the current usage count and window start time for course rate limiting.
52+
#[contracttype]
53+
#[derive(Clone, Debug, PartialEq)]
54+
pub struct CourseRateLimitData {
55+
/// Current count of course creations in this window
56+
pub count: u32,
57+
/// Timestamp when the current window started
58+
pub window_start: u64,
59+
}
60+
3361
#[contracttype]
3462
#[derive(Clone, Debug, PartialEq)]
3563
pub struct CourseCategory {
@@ -49,6 +77,10 @@ pub enum DataKey {
4977
CategorySeq, // Sequence counter for category IDs
5078
CourseCategory(u128), // Course category by ID
5179
Admins, // List of admin addresses
80+
/// Key for storing course rate limiting configuration
81+
CourseRateLimitConfig,
82+
/// Key for storing course rate limiting data per address: address -> CourseRateLimitData
83+
CourseRateLimit(Address),
5284
}
5385

5486
#[contracttype]

contracts/course/course_registry/test_snapshots/functions/add_module/test/test_add_module_invalid_course.1.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,62 @@
5151
4095
5252
]
5353
],
54+
[
55+
{
56+
"contract_data": {
57+
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
58+
"key": {
59+
"vec": [
60+
{
61+
"symbol": "CourseRateLimitConfig"
62+
}
63+
]
64+
},
65+
"durability": "persistent"
66+
}
67+
},
68+
[
69+
{
70+
"last_modified_ledger_seq": 0,
71+
"data": {
72+
"contract_data": {
73+
"ext": "v0",
74+
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4",
75+
"key": {
76+
"vec": [
77+
{
78+
"symbol": "CourseRateLimitConfig"
79+
}
80+
]
81+
},
82+
"durability": "persistent",
83+
"val": {
84+
"map": [
85+
{
86+
"key": {
87+
"symbol": "max_courses_per_window"
88+
},
89+
"val": {
90+
"u32": 3
91+
}
92+
},
93+
{
94+
"key": {
95+
"symbol": "window_seconds"
96+
},
97+
"val": {
98+
"u64": 3600
99+
}
100+
}
101+
]
102+
}
103+
}
104+
},
105+
"ext": "v0"
106+
},
107+
4095
108+
]
109+
],
54110
[
55111
{
56112
"contract_data": {

0 commit comments

Comments
 (0)