frame_plugins/penalty.rs
1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// ``````````````````````````````` PENALTY PLUGINS ```````````````````````````````
14// ===============================================================================
15
16//! Provides a suite of **pluggable penalty models** used to transform,
17//! normalize, and constrain penalty values associated with entities.
18//!
19//! These models are designed to operate as **post-processing layers**
20//! in governance, reputation, and scoring systems, ensuring that penalties
21//! remain **bounded, fair, and resistant to extreme values**.
22//!
23//! ## Design Philosophy
24//!
25//! Raw penalty values (e.g., slashing amounts, negative scores, or risk weights)
26//! can often be:
27//! - **Unbounded**: leading to disproportionate punishment
28//! - **Noisy**: zero or insignificant values
29//! - **Inconsistent**: varying widely across participants
30//!
31//! Penalty models provide a structured way to:
32//! - **Cap excessive penalties** to prevent extreme outcomes
33//! - **Enforce minimum penalties** to avoid negligible punishments
34//! - **Normalize distributions** for fair comparison across entities
35//! - **Filter irrelevant values** (e.g., zero penalties)
36//!
37//! By transforming raw penalties into **controlled and comparable values**,
38//! these plugins ensure stable and predictable behavior in downstream systems.
39//!
40//! ## Core Concepts
41//!
42//! - **Input (`p`)**: A penalty value associated with an entity.
43//! - **Output (`f(p)`)**: The transformed penalty after applying constraints.
44//! - **Penalty List**: A collection of `(Id, Penalty)` pairs.
45//! - **Context**: Optional configuration defining bounds (e.g., threshold, floor, cap).
46//!
47//! ## Applications
48//!
49//! - **Governance systems**: Slashing, penalties, or reputation decay
50//! - **Reputation engines**: Penalizing malicious or low-quality behavior
51//! - **Scoring systems**: Normalizing negative contributions
52//! - **Risk models**: Bounding downside exposure
53
54// ===============================================================================
55// ``````````````````````````````````` IMPORTS ```````````````````````````````````
56// ===============================================================================
57
58// --- Core / Std ---
59use core::iter::once;
60
61// --- FRAME Suite ---
62use frame_suite::plugin_model;
63
64// --- Substrate primitives ---
65use sp_runtime::traits::Zero;
66
67// ===============================================================================
68// `````````````````````````````` THRESHOLD-PENALTY ``````````````````````````````
69// ===============================================================================
70
71/// Configuration for [`ThresholdPenalty`] plugin.
72///
73/// Defines the **maximum allowable penalty** per entity.
74///
75/// - `threshold`: Upper bound for any individual penalty value.
76/// - Penalties above this value are **clamped down** to the threshold.
77/// - Useful for preventing excessive punishment or outliers.
78///
79/// ## Example
80/// ```ignore
81/// let config = ThresholdPenaltyConfig { threshold: 50 };
82/// ```
83pub struct ThresholdPenaltyConfig<T> {
84 pub threshold: T,
85}
86
87plugin_model!(
88 /// Applies an **upper threshold cap** to penalties.
89 ///
90 /// **Concept**: **Penalty Clamping (Upper Bound)**
91 ///
92 /// Each `(Id, Penalty)` pair is processed such that:
93 ///
94 /// ```text
95 /// f(p) = min(p, threshold)
96 /// ```
97 ///
98 /// - Zero penalties are ignored.
99 /// - Values above the threshold are reduced to the threshold.
100 ///
101 /// ## Characteristics:
102 /// - **Upper-bounded**: Prevents penalties from exceeding a maximum.
103 /// - **Noise filtering**: Removes zero penalties entirely.
104 /// - **Deterministic**: Stateless and predictable transformation.
105 ///
106 /// ## Applications:
107 /// - Limiting punishment severity in governance systems
108 /// - Anti-abuse mechanisms (prevent extreme slashing)
109 /// - Normalizing penalty distributions
110 ///
111 /// ## Example:
112 /// ```ignore
113 /// input = [(A, 10), (B, 80)]
114 /// threshold = 50
115 /// output = [(A, 10), (B, 50)]
116 /// ```
117 name: pub ThresholdPenalty,
118 input: PenaltyList,
119 others: [Id, Penalty],
120 context: ThresholdPenaltyConfig<Penalty>,
121 bounds: [
122 PenaltyList: IntoIterator<Item = (Id, Penalty)>
123 + FromIterator<(Id, Penalty)>
124 + Extend<(Id, Penalty)>
125 + Default
126 + Clone,
127 Penalty: PartialOrd + Copy + Zero,
128 ],
129 compute: |input, context| {
130 let mut result = PenaltyList::default();
131 // 1. Iterate through all penalties
132 for (id, penalty) in input.clone().into_iter() {
133 // 2. Skip zero penalties
134 if penalty.is_zero() {
135 continue;
136 }
137 // 3. Clamp penalty to threshold
138 let actual = penalty > context.threshold;
139 let new_penalty = match actual {
140 true => context.threshold,
141 false => penalty
142 };
143 // 4. Insert adjusted value
144 result.extend(once((id, new_penalty)));
145 }
146 result
147 }
148);
149
150// ===============================================================================
151// ```````````````````````````````` CAPPED-PENALTY ```````````````````````````````
152// ===============================================================================
153
154/// Configuration for [`CappedPenalty`] plugin.
155///
156/// Defines both **minimum and maximum bounds** for penalties.
157///
158/// - `floor`: Minimum penalty value (lower bound)
159/// - `cap`: Maximum penalty value (upper bound)
160///
161/// ## Example
162/// ```ignore
163/// let config = CappedPenaltyConfig { floor: 10, cap: 50 };
164/// ```
165pub struct CappedPenaltyConfig<T> {
166 pub floor: T,
167 pub cap: T,
168}
169
170plugin_model!(
171 /// Applies a **bounded range constraint** to penalties.
172 ///
173 /// **Concept**: **Range Clamping (Floor + Cap)**
174 ///
175 /// Each penalty is transformed as:
176 ///
177 /// ```text
178 /// f(p) = max(floor, min(p, cap))
179 /// ```
180 ///
181 /// - Ensures penalties stay within a defined range.
182 /// - Zero penalties are ignored.
183 ///
184 /// ## Characteristics:
185 /// - **Bi-directional bounds**: Enforces both minimum and maximum limits.
186 /// - **Stabilizing**: Prevents extremely low or high penalties.
187 /// - **Deterministic**: Stateless transformation.
188 ///
189 /// ## Applications:
190 /// - Enforcing minimum punishment levels
191 /// - Preventing extreme slashing or negligible penalties
192 /// - Maintaining consistent penalty distributions
193 ///
194 /// ## Example:
195 /// ```ignore
196 /// input = [(A, 5), (B, 100)]
197 /// floor = 10, cap = 50
198 /// output = [(A, 10), (B, 50)]
199 /// ```
200 name: pub CappedPenalty,
201 input: PenaltyList,
202 others: [Id, Penalty],
203 context: CappedPenaltyConfig<Penalty>,
204 bounds: [
205 PenaltyList: IntoIterator<Item = (Id, Penalty)>
206 + FromIterator<(Id, Penalty)>
207 + Extend<(Id, Penalty)>
208 + Default
209 + Clone,
210 Penalty: PartialOrd + Copy + Zero,
211 ],
212 compute: |input, context| {
213 let mut result = PenaltyList::default();
214 // 1. Iterate through all penalties
215 for (id, penalty) in input.clone().into_iter() {
216 // 2. Skip zero penalties
217 if penalty.is_zero() {
218 continue;
219 }
220 // 3. Apply range clamp: floor <= p <= cap
221 let adjusted_penalty = if penalty > context.cap {
222 context.cap
223 } else if penalty < context.floor {
224 context.floor
225 } else {
226 penalty
227 };
228 // 4. Insert adjusted value
229 result.extend(once((id, adjusted_penalty)));
230 }
231 result
232 }
233);
234
235// ===============================================================================
236// ````````````````````````` PENALTY MODELS PLUGIN TESTS `````````````````````````
237// ===============================================================================
238
239#[cfg(test)]
240mod tests {
241
242 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
243 // ``````````````````````````````````` IMPORTS ```````````````````````````````````
244 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
245
246 // --- Local crate imports ---
247 use super::*;
248
249 // --- FRAME Suite ---
250 use frame_suite::plugin_test;
251
252 // --- Substrate primitives ---
253 use sp_runtime::{AccountId32, Perbill};
254
255 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
256 // `````````````````````````````````` CONSTANTS ``````````````````````````````````
257 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
258
259 const fn account_frm_seed(seed: u8) -> AccountId32 {
260 let mut data = [0u8; 32];
261 data[31] = seed;
262 AccountId32::new(data)
263 }
264
265 const ALICE: AccountId32 = account_frm_seed(1);
266 const BOB: AccountId32 = account_frm_seed(2);
267 const CHARLIE: AccountId32 = account_frm_seed(3);
268 const ALAN: AccountId32 = account_frm_seed(4);
269
270 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
271 // `````````````````````````````` THRESHOLD-PENALTY ``````````````````````````````
272 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
273
274 plugin_test! {
275 model: ThresholdPenalty,
276 input: Vec<(AccountId32, Perbill)>,
277 context: ThresholdPenaltyConfig<Perbill>,
278 value: ThresholdPenaltyConfig {
279 threshold: Perbill::from_percent(70)
280 },
281 cases: {
282 (threshold_penalty_above_threshold,
283 vec![(ALICE, Perbill::from_percent(71)), (ALICE, Perbill::from_percent(80)), (ALICE, Perbill::from_percent(92))],
284 vec![(ALICE, Perbill::from_percent(70)), (ALICE, Perbill::from_percent(70)), (ALICE, Perbill::from_percent(70))]
285 ),
286 (threshold_penalty_below_threshold,
287 vec![(ALICE, Perbill::from_percent(69)), (ALICE, Perbill::from_percent(50)), (ALICE, Perbill::from_percent(25))],
288 vec![(ALICE, Perbill::from_percent(69)), (ALICE, Perbill::from_percent(50)), (ALICE, Perbill::from_percent(25))]
289 ),
290 (threshold_penalty_equal_to_threshold,
291 vec![(ALICE, Perbill::from_percent(70))],
292 vec![(ALICE, Perbill::from_percent(70))]
293 ),
294 }
295 }
296
297 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
298 // ```````````````````````````````` CAPPED-PENALTY ```````````````````````````````
299 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
300
301 plugin_test! {
302 model: CappedPenalty,
303 input: Vec<(AccountId32, Perbill)>,
304 context: CappedPenaltyConfig<Perbill>,
305 value: CappedPenaltyConfig {
306 floor: Perbill::from_percent(25),
307 cap: Perbill::from_percent(75),
308 },
309 cases: {
310 (capped_penalty_above_cap,
311 vec![(ALICE, Perbill::from_percent(76)), (BOB, Perbill::from_percent(80)), (ALAN, Perbill::from_percent(92))],
312 vec![(ALICE, Perbill::from_percent(75)), (BOB, Perbill::from_percent(75)), (ALAN, Perbill::from_percent(75))]
313 ),
314 (capped_penalty_below_floor,
315 vec![(ALICE, Perbill::from_percent(24)), (BOB, Perbill::from_percent(17)), (ALAN, Perbill::from_percent(6))],
316 vec![(ALICE, Perbill::from_percent(25)), (BOB, Perbill::from_percent(25)), (ALAN, Perbill::from_percent(25))]
317 ),
318 (capped_penalty_inbetween_cap_and_floor,
319 vec![(ALICE, Perbill::from_percent(26)), (BOB, Perbill::from_percent(52)), (CHARLIE, Perbill::from_percent(34)), (ALAN, Perbill::from_percent(74))],
320 vec![(ALICE, Perbill::from_percent(26)), (BOB, Perbill::from_percent(52)), (CHARLIE, Perbill::from_percent(34)), (ALAN, Perbill::from_percent(74))]
321 ),
322 (capped_penalty_equal_to_cap_and_floor,
323 vec![(ALICE, Perbill::from_percent(25)), (ALICE, Perbill::from_percent(75))],
324 vec![(ALICE, Perbill::from_percent(25)), (ALICE, Perbill::from_percent(75))]
325 ),
326 }
327 }
328}