penumbra_sdk_stake/undelegate_claim/
proof.rs1use 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#[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#[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 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 #[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}