penumbra_sdk_stake/undelegate_claim/
plan.rs

1use decaf377::{Fq, Fr};
2use penumbra_sdk_asset::{asset, balance, Balance};
3use penumbra_sdk_num::Amount;
4use penumbra_sdk_proof_params::CONVERT_PROOF_PROVING_KEY;
5use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    IdentityKey, Penalty, UnbondingToken, UndelegateClaim, UndelegateClaimBody,
11    UndelegateClaimProof,
12};
13
14use super::UndelegateClaimProofPublic;
15
16#[derive(Clone, Debug, Deserialize, Serialize)]
17#[serde(try_from = "pb::UndelegateClaimPlan", into = "pb::UndelegateClaimPlan")]
18pub struct UndelegateClaimPlan {
19    /// The identity key of the validator to undelegate from.
20    pub validator_identity: IdentityKey,
21    /// The penalty applied to undelegation, in bps^2.
22    pub penalty: Penalty,
23    /// The amount of unbonding tokens to claim. This is a bare number because its denom is determined by the preceding data.
24    pub unbonding_amount: Amount,
25    /// The blinding factor that will be used for the balance commitment.
26    /// The action's contribution to the transaction's value balance.
27    pub balance_blinding: Fr,
28    /// The first blinding factor used for generating the ZK proof.
29    pub proof_blinding_r: Fq,
30    /// The second blinding factor used for generating the ZK proof.
31    pub proof_blinding_s: Fq,
32    /// The height at which unbonding began, used to verify the penalty and unbonding delay.
33    pub unbonding_start_height: u64,
34}
35
36impl UndelegateClaimPlan {
37    /// Convenience method to construct the [`UndelegateClaim`] described by this [`UndelegateClaimPlan`].
38    pub fn undelegate_claim(&self) -> UndelegateClaim {
39        UndelegateClaim {
40            body: self.undelegate_claim_body(),
41            proof: self.undelegate_claim_proof(),
42        }
43    }
44
45    /// Construct the [`UndelegateClaimBody`] described by this [`UndelegateClaimPlan`].
46    pub fn undelegate_claim_body(&self) -> UndelegateClaimBody {
47        UndelegateClaimBody {
48            validator_identity: self.validator_identity,
49            unbonding_start_height: self.unbonding_start_height,
50            penalty: self.penalty,
51            balance_commitment: self.balance().commit(self.balance_blinding),
52        }
53    }
54
55    /// Construct the [`UndelegateClaimProof`] required by the [`UndelegateClaimBody`] described by this [`UndelegateClaimPlan`].
56    pub fn undelegate_claim_proof(&self) -> UndelegateClaimProof {
57        UndelegateClaimProof::prove(
58            self.proof_blinding_r,
59            self.proof_blinding_s,
60            &CONVERT_PROOF_PROVING_KEY,
61            UndelegateClaimProofPublic {
62                balance_commitment: self.balance_commitment(),
63                unbonding_id: self.unbonding_id(),
64                penalty: self.penalty,
65            },
66            super::UndelegateClaimProofPrivate {
67                unbonding_amount: self.unbonding_amount,
68                balance_blinding: self.balance_blinding,
69            },
70        )
71        .expect("can generate undelegate claim proof")
72    }
73
74    pub fn unbonding_token(&self) -> UnbondingToken {
75        UnbondingToken::new(self.validator_identity, self.unbonding_start_height)
76    }
77
78    pub fn unbonding_id(&self) -> asset::Id {
79        self.unbonding_token().id()
80    }
81
82    pub fn balance_commitment(&self) -> balance::Commitment {
83        self.balance().commit(self.balance_blinding)
84    }
85
86    pub fn balance(&self) -> Balance {
87        self.penalty
88            .balance_for_claim(self.unbonding_id(), self.unbonding_amount)
89    }
90}
91
92impl DomainType for UndelegateClaimPlan {
93    type Proto = pb::UndelegateClaimPlan;
94}
95
96impl From<UndelegateClaimPlan> for pb::UndelegateClaimPlan {
97    #[allow(deprecated)]
98    fn from(msg: UndelegateClaimPlan) -> Self {
99        Self {
100            validator_identity: Some(msg.validator_identity.into()),
101            start_epoch_index: 0,
102            penalty: Some(msg.penalty.into()),
103            unbonding_amount: Some(msg.unbonding_amount.into()),
104            unbonding_start_height: msg.unbonding_start_height,
105            balance_blinding: msg.balance_blinding.to_bytes().to_vec(),
106            proof_blinding_r: msg.proof_blinding_r.to_bytes().to_vec(),
107            proof_blinding_s: msg.proof_blinding_s.to_bytes().to_vec(),
108        }
109    }
110}
111
112impl TryFrom<pb::UndelegateClaimPlan> for UndelegateClaimPlan {
113    type Error = anyhow::Error;
114    fn try_from(msg: pb::UndelegateClaimPlan) -> Result<Self, Self::Error> {
115        let proof_blinding_r_bytes: [u8; 32] = msg
116            .proof_blinding_r
117            .try_into()
118            .map_err(|_| anyhow::anyhow!("malformed r in `UndelegateClaimPlan`"))?;
119        let proof_blinding_s_bytes: [u8; 32] = msg
120            .proof_blinding_s
121            .try_into()
122            .map_err(|_| anyhow::anyhow!("malformed s in `UndelegateClaimPlan`"))?;
123
124        Ok(Self {
125            validator_identity: msg
126                .validator_identity
127                .ok_or_else(|| anyhow::anyhow!("missing validator_identity"))?
128                .try_into()?,
129            penalty: msg
130                .penalty
131                .ok_or_else(|| anyhow::anyhow!("missing penalty"))?
132                .try_into()?,
133            unbonding_amount: msg
134                .unbonding_amount
135                .ok_or_else(|| anyhow::anyhow!("missing unbonding_amount"))?
136                .try_into()?,
137            balance_blinding: Fr::from_bytes_checked(
138                msg.balance_blinding
139                    .as_slice()
140                    .try_into()
141                    .map_err(|_| anyhow::anyhow!("expected 32 bytes"))?,
142            )
143            .map_err(|_| anyhow::anyhow!("invalid balance_blinding"))?,
144            proof_blinding_r: Fq::from_bytes_checked(&proof_blinding_r_bytes)
145                .expect("proof_blinding_r malformed"),
146            proof_blinding_s: Fq::from_bytes_checked(&proof_blinding_s_bytes)
147                .expect("proof_blinding_s malformed"),
148            unbonding_start_height: msg.unbonding_start_height,
149        })
150    }
151}