penumbra_stake/undelegate_claim/
proof.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
use decaf377::Bls12_377;

use ark_groth16::{PreparedVerifyingKey, ProvingKey};
use base64::prelude::*;
use penumbra_proto::{core::component::stake::v1 as pb, DomainType};

use decaf377::{Fq, Fr};
use penumbra_asset::{asset, balance, STAKING_TOKEN_ASSET_ID};
use penumbra_num::Amount;
use penumbra_proof_params::VerifyingKeyExt;
use penumbra_shielded_pool::{ConvertProof, ConvertProofPrivate, ConvertProofPublic};

use crate::Penalty;

/// The public inputs to an [`UndelegateClaimProof`].
#[derive(Clone, Debug)]
pub struct UndelegateClaimProofPublic {
    pub balance_commitment: balance::Commitment,
    pub unbonding_id: asset::Id,
    pub penalty: Penalty,
}

impl From<UndelegateClaimProofPublic> for ConvertProofPublic {
    fn from(value: UndelegateClaimProofPublic) -> Self {
        Self {
            from: value.unbonding_id,
            to: *STAKING_TOKEN_ASSET_ID,
            rate: value.penalty.kept_rate(),
            balance_commitment: value.balance_commitment,
        }
    }
}

/// The private inputs to an [`UndelegateClaimProof`].
#[derive(Clone, Debug)]
pub struct UndelegateClaimProofPrivate {
    pub unbonding_amount: Amount,
    pub balance_blinding: Fr,
}

impl From<UndelegateClaimProofPrivate> for ConvertProofPrivate {
    fn from(value: UndelegateClaimProofPrivate) -> Self {
        Self {
            amount: value.unbonding_amount,
            balance_blinding: value.balance_blinding,
        }
    }
}

#[derive(Clone, Debug)]
pub struct UndelegateClaimProof(ConvertProof);

impl UndelegateClaimProof {
    #![allow(clippy::too_many_arguments)]
    /// Generate an `UndelegateClaimProof` given the proving key, public inputs,
    /// witness data, and two random elements `blinding_r` and `blinding_s`.
    pub fn prove(
        blinding_r: Fq,
        blinding_s: Fq,
        pk: &ProvingKey<Bls12_377>,
        public: UndelegateClaimProofPublic,
        private: UndelegateClaimProofPrivate,
    ) -> anyhow::Result<Self> {
        let proof = ConvertProof::prove(blinding_r, blinding_s, pk, public.into(), private.into())?;
        Ok(Self(proof))
    }

    /// Called to verify the proof using the provided public inputs.
    #[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(self.clone().encode_to_vec()), vk = ?vk.debug_id()))]
    pub fn verify(
        &self,
        vk: &PreparedVerifyingKey<Bls12_377>,
        public: UndelegateClaimProofPublic,
    ) -> anyhow::Result<()> {
        self.0.verify(vk, public.into())
    }
}

impl DomainType for UndelegateClaimProof {
    type Proto = pb::ZkUndelegateClaimProof;
}

impl From<UndelegateClaimProof> for pb::ZkUndelegateClaimProof {
    fn from(proof: UndelegateClaimProof) -> Self {
        pb::ZkUndelegateClaimProof {
            inner: proof.0.to_vec(),
        }
    }
}

impl TryFrom<pb::ZkUndelegateClaimProof> for UndelegateClaimProof {
    type Error = anyhow::Error;

    fn try_from(proto: pb::ZkUndelegateClaimProof) -> Result<Self, Self::Error> {
        Ok(UndelegateClaimProof(proto.inner[..].try_into()?))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use decaf377::{Fq, Fr};
    use decaf377_rdsa as rdsa;
    use penumbra_num::Amount;
    use penumbra_proof_params::generate_prepared_test_parameters;
    use proptest::prelude::*;
    use rand_core::OsRng;
    use rdsa::VerificationKey;

    use crate::{IdentityKey, Penalty, UnbondingToken};
    use penumbra_shielded_pool::ConvertCircuit;

    fn fr_strategy() -> BoxedStrategy<Fr> {
        any::<[u8; 32]>()
            .prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
            .boxed()
    }

    proptest! {
    #![proptest_config(ProptestConfig::with_cases(2))]
    #[test]
    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) {
            let mut rng = OsRng;
            let (pk, vk) = generate_prepared_test_parameters::<ConvertCircuit>(&mut rng);

            let sk = rdsa::SigningKey::new_from_field(validator_randomness);
            let validator_identity = IdentityKey(VerificationKey::from(&sk).into());
            let unbonding_amount = Amount::from(value1_amount);

            let start_epoch_index = 1;
            let unbonding_token = UnbondingToken::new(validator_identity, start_epoch_index);
            let unbonding_id = unbonding_token.id();
            let penalty = Penalty::from_percent(penalty_amount);
            let balance = penalty.balance_for_claim(unbonding_id, unbonding_amount);
            let balance_commitment = balance.commit(balance_blinding);

            let public = UndelegateClaimProofPublic { balance_commitment, unbonding_id, penalty };
            let private = UndelegateClaimProofPrivate { unbonding_amount, balance_blinding };

            let blinding_r = Fq::rand(&mut rng);
            let blinding_s = Fq::rand(&mut rng);
            let proof = UndelegateClaimProof::prove(
                blinding_r,
                blinding_s,
                &pk,
                public.clone(),
                private
            )
            .expect("can create proof");

            let proof_result = proof.verify(&vk, public);

            assert!(proof_result.is_ok());
        }
    }
}