Skip to main content

psibase/services/
diff_adjust.rs

1//! # Description
2//!
3//! Dynamic difficulty adjustment service for adaptive rate limiting
4//!
5//! This service provides a self-adjusting rate limiting mechanism that dynamically modifies
6//! difficulty thresholds based on observed activity patterns. Each rate limiter is represented
7//! by an NFT, allowing ownership-based administration.
8//!
9//! # Mechanics
10//!
11//! The service operates on a time-windowed activity count:
12//!
13//! 1. **Activity Tracking**: A consumer account increments `activity_count` each time an action
14//!    occurs. `activity_count` accumulates within a configurable time window.
15//!
16//! 2. **Difficulty Adjustment**:
17//!    - **Below Target**: If `activity_count` is below `target_min` when a window elapses, the
18//!      difficulty decreases by a configured percentage (subject to a floor value).
19//!    - **Above Target**: If `activity_count` exceeds `target_max` at any point, the difficulty
20//!      immediately increases by a configured percentage, `activity_count` resets, and a new window
21//!      begins. If one increment pushes `activity_count` past `target_max` by one or more whole
22//!      multiples of `target_max`, the difficulty is increased by the configured percentage once per multiple.
23//!
24//! 3. **Window-Based Decay**: After each window period (`window_seconds`), if activity was below
25//!    the minimum target, difficulty decays proportionally for each complete window that elapsed.
26//!
27//! 4. **Bounded Adjustments**: Difficulty cannot fall below `floor_difficulty`, ensuring a minimum
28//!    security threshold is maintained.
29//!
30//! # Roles
31//!
32//! - **Admin**: The NFT owner can configure all parameters (targets, window, percentages, floor).
33//!   The NFT is initially minted to the account that creates the rate limiter, but can be
34//!   transferred to change administration.
35//! - **Consumer**: The account designated at creation time can increment the activity_count and query
36//!   difficulty. The consumer is automatically set to the account that calls `create()` and cannot
37//!   be changed after creation. Initially, the creator is both admin and consumer, but the admin
38//!   role can be transferred via NFT ownership while the consumer remains fixed.
39
40#[crate::service(name = "diff-adj", dispatch = false, psibase_mod = "crate")]
41#[allow(non_snake_case, unused_variables)]
42pub mod Service {
43    use crate::{AccountNumber, Pack, TimePointSec, ToSchema, Unpack};
44
45    use async_graphql::SimpleObject;
46    use serde::{Deserialize, Serialize};
47
48    #[table(name = "RateLimitTable", index = 0)]
49    #[derive(Default, Pack, Unpack, ToSchema, SimpleObject, Serialize, Deserialize, Debug)]
50    #[fracpack(fracpack_mod = "fracpack")]
51    pub struct RateLimit {
52        #[primary_key]
53        pub nft_id: u32,
54        pub window_seconds: u32,
55        pub activity_count: u32,
56        pub target_min: u32,
57        pub target_max: u32,
58        pub floor_difficulty: u64,
59        pub active_difficulty: u64,
60        pub last_update: TimePointSec,
61        pub increase_ppm: u32,
62        pub decrease_ppm: u32,
63        pub consumer: AccountNumber,
64    }
65
66    /// Creates a new Rate limit
67    ///
68    /// # Arguments
69    /// * `initial_difficulty` - Sets initial difficulty
70    /// * `window_seconds` - Seconds duration before decay occurs
71    /// * `target_min` - Minimum rate limit target
72    /// * `target_max` - Maximum rate limit target
73    /// * `floor_difficulty` - Minimum difficulty
74    /// * `increase_ppm` - PPM to increase when over target, e.g. 50000 ppm = 5%
75    /// * `decrease_ppm` - PPM to decrease when under target, e.g. 50000 ppm = 5%
76    #[action]
77    fn create(
78        initial_difficulty: u64,
79        window_seconds: u32,
80        target_min: u32,
81        target_max: u32,
82        floor_difficulty: u64,
83        increase_ppm: u32,
84        decrease_ppm: u32,
85    ) -> u32 {
86        unimplemented!()
87    }
88
89    /// Get RateLimit record
90    ///
91    /// # Arguments
92    /// * `nft_id` - RateLimit / NFT ID
93    ///
94    /// # Returns
95    /// The RateLimit if it exists
96    #[action]
97    fn get(nft_id: u32) -> Option<RateLimit> {
98        unimplemented!()
99    }
100
101    /// Get RateLimit difficulty
102    ///
103    /// # Arguments
104    /// * `nft_id` - RateLimit / NFT ID
105    ///
106    /// # Returns
107    /// Difficulty of RateLimit
108    #[action]
109    fn get_diff(nft_id: u32) -> u64 {
110        unimplemented!()
111    }
112
113    /// Increment RateLimit instance, potentially increasing the difficulty.
114    ///
115    /// The difficulty may increase multiple times if `activity_count` exceeds `target_max`
116    ///   by more than one multiple of `target_max`.
117    ///
118    /// Returns the difficulty before any difficulty adjustment due to the increment.
119    ///
120    /// * Requires sender to be consumer account.
121    ///
122    /// # Arguments
123    /// * `nft_id` - RateLimit / NFT ID
124    /// * `amount` - Amount to increment the activity_count by
125    #[action]
126    fn increment(nft_id: u32, amount: u32) -> u64 {
127        unimplemented!()
128    }
129
130    /// Update targets
131    ///
132    /// * Requires holding administration NFT.
133    ///
134    /// # Arguments
135    /// * `nft_id` - RateLimit / NFT ID
136    /// * `target_min` - Minimum target activity
137    /// * `target_max` - Maximum target activity
138    #[action]
139    fn set_targets(nft_id: u32, target_min: u32, target_max: u32) {
140        unimplemented!()
141    }
142
143    /// Update window
144    ///
145    /// * Requires holding administration NFT.
146    ///
147    /// # Arguments
148    /// * `nft_id` - RateLimit / NFT ID
149    /// * `seconds` - Seconds
150    #[action]
151    fn set_window(nft_id: u32, seconds: u32) {
152        unimplemented!()
153    }
154
155    /// Update floor difficulty
156    ///
157    /// * Requires holding administration NFT.
158    ///
159    /// # Arguments
160    /// * `nft_id` - RateLimit / NFT ID
161    /// * `difficulty` - Difficulty
162    #[action]
163    fn set_floor(nft_id: u32, difficulty: u64) {
164        unimplemented!()
165    }
166
167    /// Update ppm change
168    ///
169    /// * Requires holding administration NFT.
170    ///
171    /// # Arguments
172    /// * `nft_id` - RateLimit / NFT ID
173    /// * `increase_ppm` - PPM to increase when over target, e.g. 50000 ppm = 5%
174    /// * `decrease_ppm` - PPM to decrease when under target, e.g. 50000 ppm = 5%
175    #[action]
176    fn set_ppm(nft_id: u32, increase_ppm: u32, decrease_ppm: u32) {
177        unimplemented!()
178    }
179
180    /// Delete RateLimit instance
181    ///
182    /// * Requires holding administration NFT.
183    ///
184    /// # Arguments
185    /// * `nft_id` - RateLimit / NFT ID
186    #[action]
187    fn delete(nft_id: u32) {
188        unimplemented!()
189    }
190
191    /// Get targets
192    ///
193    /// Gets the minimum and maximum targets for the specified DiffAdjust
194    ///
195    /// Returns (target_min, target_max)
196    #[action]
197    fn get_targets(nft_id: u32) -> (u32, u32) {
198        unimplemented!()
199    }
200}
201
202pub use Service::{RateLimit, RateLimitTable};
203
204#[test]
205fn verify_schema() {
206    crate::assert_schema_matches_package::<Wrapper>();
207}