penumbra_sdk_stake/
penalty.rs

1use ark_ff::ToConstraintField;
2use decaf377::Fq;
3use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pbs, DomainType};
4use serde::{Deserialize, Serialize};
5
6use penumbra_sdk_asset::{asset, Balance, Value, STAKING_TOKEN_ASSET_ID};
7use penumbra_sdk_num::{fixpoint::U128x128, Amount};
8
9/// Tracks slashing penalties applied to a validator in some epoch.
10///
11/// You do not need to know how the penalty is represented.
12///
13/// If you insist on knowing, it's represented as a U128x128 between 0 and 1,
14/// which denotes the amount *kept* after applying a penalty. e.g. a 1% penalty
15/// would be 0.99.
16#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
17#[serde(try_from = "pbs::Penalty", into = "pbs::Penalty")]
18pub struct Penalty(U128x128);
19
20impl Penalty {
21    /// Create a `Penalty` from a percentage e.g.
22    /// `Penalty::from_percent(1)` is a 1% penalty.
23    /// `Penalty::from_percent(100)` is a 100% penalty.
24    pub fn from_percent(percent: u64) -> Self {
25        Penalty::from_bps(percent.saturating_mul(100))
26    }
27
28    /// Create a `Penalty` from a basis point e.g.
29    /// `Penalty::from_bps(1)` is a 1 bps penalty.
30    /// `Penalty::from_bps(100)` is a 100 bps penalty.
31    pub fn from_bps(bps: u64) -> Self {
32        Penalty::from_bps_squared(bps.saturating_mul(10000))
33    }
34
35    /// Create a `Penalty` from a basis point squared e.g.
36    /// `Penalty::from_bps(1_0000_0000)` is a 100% penalty.
37    pub fn from_bps_squared(bps_squared: u64) -> Self {
38        assert!(bps_squared <= 1_0000_0000);
39        Self(U128x128::ratio(bps_squared, 1_0000_0000).expect(&format!(
40            "{bps_squared} bps^2 should be convertible to a U128x128"
41        )))
42        .one_minus_this()
43    }
44
45    fn one_minus_this(&self) -> Penalty {
46        Self(
47            (U128x128::from(1u64) - self.0)
48                .expect("1 - penalty should never underflow, because penalty is at most 1"),
49        )
50    }
51
52    /// A rate representing how much of an asset remains after applying a penalty.
53    ///
54    /// e.g. a 1% penalty will yield a rate of 0.99 here.
55    pub fn kept_rate(&self) -> U128x128 {
56        self.0
57    }
58
59    /// Compound this `Penalty` with another `Penalty`.
60    pub fn compound(&self, other: Penalty) -> Penalty {
61        Self((self.0 * other.0).expect("compounding penalties will not overflow"))
62    }
63
64    /// Apply this `Penalty` to an `Amount` of unbonding tokens.
65    pub fn apply_to_amount(&self, amount: Amount) -> Amount {
66        self.0
67            .apply_to_amount(&amount)
68            .expect("should not overflow, because penalty is <= 1")
69    }
70
71    /// Apply this `Penalty` to some fraction.
72    pub fn apply_to(&self, amount: impl Into<U128x128>) -> U128x128 {
73        (amount.into() * self.0).expect("should not overflow, because penalty is <= 1")
74    }
75
76    /// Helper method to compute the effect of an UndelegateClaim on the
77    /// transaction's value balance, used in planning and (transparent) proof
78    /// verification.
79    ///
80    /// This method takes the `unbonding_id` rather than the `UnbondingToken` so
81    /// that it can be used in mock proof verification, where computation of the
82    /// unbonding token's asset ID happens outside of the circuit.
83    pub fn balance_for_claim(&self, unbonding_id: asset::Id, unbonding_amount: Amount) -> Balance {
84        // The undelegate claim action subtracts the unbonding amount and adds
85        // the unbonded amount from the transaction's value balance.
86        Balance::zero()
87            - Value {
88                amount: unbonding_amount,
89                asset_id: unbonding_id,
90            }
91            + Value {
92                amount: self.apply_to_amount(unbonding_amount),
93                asset_id: *STAKING_TOKEN_ASSET_ID,
94            }
95    }
96}
97
98impl ToConstraintField<Fq> for Penalty {
99    fn to_field_elements(&self) -> Option<Vec<Fq>> {
100        self.0.to_field_elements()
101    }
102}
103
104impl From<Penalty> for [u8; 32] {
105    fn from(value: Penalty) -> Self {
106        value.0.into()
107    }
108}
109
110impl<'a> TryFrom<&'a [u8]> for Penalty {
111    type Error = <U128x128 as TryFrom<&'a [u8]>>::Error;
112
113    fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
114        U128x128::try_from(value).map(Self)
115    }
116}
117
118impl DomainType for Penalty {
119    type Proto = pbs::Penalty;
120}
121
122impl From<Penalty> for pbs::Penalty {
123    fn from(v: Penalty) -> Self {
124        pbs::Penalty {
125            inner: <[u8; 32]>::from(v).to_vec(),
126        }
127    }
128}
129
130impl TryFrom<pbs::Penalty> for Penalty {
131    type Error = anyhow::Error;
132    fn try_from(v: pbs::Penalty) -> Result<Self, Self::Error> {
133        Ok(Penalty::try_from(v.inner.as_slice())?)
134    }
135}