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}