penumbra_sdk_stake/undelegate_claim/
proof.rs

1use decaf377::Bls12_377;
2
3use ark_groth16::{PreparedVerifyingKey, ProvingKey};
4use base64::prelude::*;
5use penumbra_sdk_proto::{core::component::stake::v1 as pb, DomainType};
6
7use decaf377::{Fq, Fr};
8use penumbra_sdk_asset::{asset, balance, STAKING_TOKEN_ASSET_ID};
9use penumbra_sdk_num::Amount;
10use penumbra_sdk_proof_params::VerifyingKeyExt;
11use penumbra_sdk_shielded_pool::{ConvertProof, ConvertProofPrivate, ConvertProofPublic};
12
13use crate::Penalty;
14
15/// The public inputs to an [`UndelegateClaimProof`].
16#[derive(Clone, Debug)]
17pub struct UndelegateClaimProofPublic {
18    pub balance_commitment: balance::Commitment,
19    pub unbonding_id: asset::Id,
20    pub penalty: Penalty,
21}
22
23impl From<UndelegateClaimProofPublic> for ConvertProofPublic {
24    fn from(value: UndelegateClaimProofPublic) -> Self {
25        Self {
26            from: value.unbonding_id,
27            to: *STAKING_TOKEN_ASSET_ID,
28            rate: value.penalty.kept_rate(),
29            balance_commitment: value.balance_commitment,
30        }
31    }
32}
33
34/// The private inputs to an [`UndelegateClaimProof`].
35#[derive(Clone, Debug)]
36pub struct UndelegateClaimProofPrivate {
37    pub unbonding_amount: Amount,
38    pub balance_blinding: Fr,
39}
40
41impl From<UndelegateClaimProofPrivate> for ConvertProofPrivate {
42    fn from(value: UndelegateClaimProofPrivate) -> Self {
43        Self {
44            amount: value.unbonding_amount,
45            balance_blinding: value.balance_blinding,
46        }
47    }
48}
49
50#[derive(Clone, Debug)]
51pub struct UndelegateClaimProof(ConvertProof);
52
53impl UndelegateClaimProof {
54    #![allow(clippy::too_many_arguments)]
55    /// Generate an `UndelegateClaimProof` given the proving key, public inputs,
56    /// witness data, and two random elements `blinding_r` and `blinding_s`.
57    pub fn prove(
58        blinding_r: Fq,
59        blinding_s: Fq,
60        pk: &ProvingKey<Bls12_377>,
61        public: UndelegateClaimProofPublic,
62        private: UndelegateClaimProofPrivate,
63    ) -> anyhow::Result<Self> {
64        let proof = ConvertProof::prove(blinding_r, blinding_s, pk, public.into(), private.into())?;
65        Ok(Self(proof))
66    }
67
68    /// Called to verify the proof using the provided public inputs.
69    #[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(self.clone().encode_to_vec()), vk = ?vk.debug_id()))]
70    pub fn verify(
71        &self,
72        vk: &PreparedVerifyingKey<Bls12_377>,
73        public: UndelegateClaimProofPublic,
74    ) -> anyhow::Result<()> {
75        self.0.verify(vk, public.into())
76    }
77}
78
79impl DomainType for UndelegateClaimProof {
80    type Proto = pb::ZkUndelegateClaimProof;
81}
82
83impl From<UndelegateClaimProof> for pb::ZkUndelegateClaimProof {
84    fn from(proof: UndelegateClaimProof) -> Self {
85        pb::ZkUndelegateClaimProof {
86            inner: proof.0.to_vec(),
87        }
88    }
89}
90
91impl TryFrom<pb::ZkUndelegateClaimProof> for UndelegateClaimProof {
92    type Error = anyhow::Error;
93
94    fn try_from(proto: pb::ZkUndelegateClaimProof) -> Result<Self, Self::Error> {
95        Ok(UndelegateClaimProof(proto.inner[..].try_into()?))
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use decaf377::{Fq, Fr};
103    use decaf377_rdsa as rdsa;
104    use penumbra_sdk_num::Amount;
105    use penumbra_sdk_proof_params::generate_prepared_test_parameters;
106    use proptest::prelude::*;
107    use rand_core::OsRng;
108    use rdsa::VerificationKey;
109
110    use crate::{IdentityKey, Penalty, UnbondingToken};
111    use penumbra_sdk_shielded_pool::ConvertCircuit;
112
113    fn fr_strategy() -> BoxedStrategy<Fr> {
114        any::<[u8; 32]>()
115            .prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
116            .boxed()
117    }
118
119    proptest! {
120    #![proptest_config(ProptestConfig::with_cases(2))]
121    #[test]
122    fn undelegate_claim_proof_happy_path(validator_randomness in fr_strategy(), balance_blinding in fr_strategy(), value1_amount in 2..200u64, penalty_amount in 0..100u64) {
123            let mut rng = OsRng;
124            let (pk, vk) = generate_prepared_test_parameters::<ConvertCircuit>(&mut rng);
125
126            let sk = rdsa::SigningKey::new_from_field(validator_randomness);
127            let validator_identity = IdentityKey(VerificationKey::from(&sk).into());
128            let unbonding_amount = Amount::from(value1_amount);
129
130            let start_epoch_index = 1;
131            let unbonding_token = UnbondingToken::new(validator_identity, start_epoch_index);
132            let unbonding_id = unbonding_token.id();
133            let penalty = Penalty::from_percent(penalty_amount);
134            let balance = penalty.balance_for_claim(unbonding_id, unbonding_amount);
135            let balance_commitment = balance.commit(balance_blinding);
136
137            let public = UndelegateClaimProofPublic { balance_commitment, unbonding_id, penalty };
138            let private = UndelegateClaimProofPrivate { unbonding_amount, balance_blinding };
139
140            let blinding_r = Fq::rand(&mut rng);
141            let blinding_s = Fq::rand(&mut rng);
142            let proof = UndelegateClaimProof::prove(
143                blinding_r,
144                blinding_s,
145                &pk,
146                public.clone(),
147                private
148            )
149            .expect("can create proof");
150
151            let proof_result = proof.verify(&vk, public);
152
153            assert!(proof_result.is_ok());
154        }
155    }
156}