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}