penumbra_sdk_governance/delegator_vote/
plan.rs

1use ark_ff::Zero;
2use decaf377::{Fq, Fr};
3use decaf377_rdsa::{Signature, SpendAuth};
4use penumbra_sdk_asset::{Balance, Value};
5use penumbra_sdk_keys::FullViewingKey;
6use penumbra_sdk_num::Amount;
7use penumbra_sdk_proof_params::DELEGATOR_VOTE_PROOF_PROVING_KEY;
8use penumbra_sdk_proto::{core::component::governance::v1 as pb, DomainType};
9use penumbra_sdk_sct::Nullifier;
10use penumbra_sdk_shielded_pool::Note;
11use penumbra_sdk_tct as tct;
12use rand::{CryptoRng, RngCore};
13use serde::{Deserialize, Serialize};
14
15use crate::delegator_vote::action::{DelegatorVote, DelegatorVoteBody};
16use crate::delegator_vote::proof::DelegatorVoteProof;
17use crate::DelegatorVoteProofPrivate;
18use crate::DelegatorVoteProofPublic;
19use crate::{vote::Vote, VotingReceiptToken};
20
21/// A plan to vote as a delegator.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(try_from = "pb::DelegatorVotePlan", into = "pb::DelegatorVotePlan")]
24pub struct DelegatorVotePlan {
25    /// The proposal ID to vote on.
26    pub proposal: u64,
27    /// The start position of the proposal.
28    pub start_position: tct::Position,
29    /// The vote to cast.
30    pub vote: Vote,
31    /// A staked note that was spendable before the proposal started.
32    pub staked_note: Note,
33    /// The unbonded amount corresponding to the staked note.
34    pub unbonded_amount: Amount,
35    /// The position of the staked note.
36    pub position: tct::Position,
37    /// The randomizer to use.
38    pub randomizer: Fr,
39    /// The first blinding factor used for generating the ZK proof.
40    pub proof_blinding_r: Fq,
41    /// The second blinding factor used for generating the ZK proof.
42    pub proof_blinding_s: Fq,
43}
44
45impl DelegatorVotePlan {
46    /// Create a new [`DelegatorVotePlan`] that votes using the given positioned `note`.
47    #[allow(clippy::too_many_arguments)]
48    pub fn new<R: CryptoRng + RngCore>(
49        rng: &mut R,
50        proposal: u64,
51        start_position: tct::Position,
52        vote: Vote,
53        staked_note: Note,
54        position: tct::Position,
55        unbonded_amount: Amount,
56    ) -> DelegatorVotePlan {
57        DelegatorVotePlan {
58            proposal,
59            start_position,
60            vote,
61            staked_note,
62            unbonded_amount,
63            position,
64            randomizer: Fr::rand(rng),
65            proof_blinding_r: Fq::rand(rng),
66            proof_blinding_s: Fq::rand(rng),
67        }
68    }
69
70    /// Convenience method to construct the [`DelegatorVote`] described by this [`DelegatorVotePlan`].
71    pub fn delegator_vote(
72        &self,
73        fvk: &FullViewingKey,
74        auth_sig: Signature<SpendAuth>,
75        auth_path: tct::Proof,
76    ) -> DelegatorVote {
77        DelegatorVote {
78            body: self.delegator_vote_body(fvk),
79            auth_sig,
80            proof: self.delegator_vote_proof(fvk, auth_path),
81        }
82    }
83
84    /// Construct the [`DelegatorVoteBody`] described by this [`DelegatorVotePlan`].
85    pub fn delegator_vote_body(&self, fvk: &FullViewingKey) -> DelegatorVoteBody {
86        DelegatorVoteBody {
87            proposal: self.proposal,
88            start_position: self.start_position,
89            vote: self.vote,
90            value: self.staked_note.value(),
91            unbonded_amount: self.unbonded_amount,
92            nullifier: Nullifier::derive(
93                fvk.nullifier_key(),
94                self.position,
95                &self.staked_note.commit(),
96            ),
97            rk: fvk.spend_verification_key().randomize(&self.randomizer),
98        }
99    }
100
101    /// Construct the [`DelegatorVoteProof`] required by the [`DelegatorVoteBody`] described by this [`DelegatorVotePlan`].
102    pub fn delegator_vote_proof(
103        &self,
104        fvk: &FullViewingKey,
105        state_commitment_proof: tct::Proof,
106    ) -> DelegatorVoteProof {
107        let public = DelegatorVoteProofPublic {
108            anchor: state_commitment_proof.root(),
109            balance_commitment: self.staked_note.value().commit(Fr::zero()),
110            nullifier: self.nullifier(fvk),
111            rk: self.rk(fvk),
112            start_position: self.start_position,
113        };
114        let private = DelegatorVoteProofPrivate {
115            state_commitment_proof,
116            note: self.staked_note.clone(),
117            v_blinding: Fr::from(0u64),
118            spend_auth_randomizer: self.randomizer,
119            ak: *fvk.spend_verification_key(),
120            nk: *fvk.nullifier_key(),
121        };
122        DelegatorVoteProof::prove(
123            self.proof_blinding_r,
124            self.proof_blinding_s,
125            &DELEGATOR_VOTE_PROOF_PROVING_KEY,
126            public,
127            private,
128        )
129        .expect("can generate ZK delegator vote proof")
130    }
131
132    /// Construct the randomized verification key associated with this [`DelegatorVotePlan`].
133    pub fn rk(&self, fvk: &FullViewingKey) -> decaf377_rdsa::VerificationKey<SpendAuth> {
134        fvk.spend_verification_key().randomize(&self.randomizer)
135    }
136
137    /// Construct the [`Nullifier`] associated with this [`DelegatorVotePlan`].
138    pub fn nullifier(&self, fvk: &FullViewingKey) -> Nullifier {
139        Nullifier::derive(
140            fvk.nullifier_key(),
141            self.position,
142            &self.staked_note.commit(),
143        )
144    }
145
146    pub fn balance(&self) -> Balance {
147        Value {
148            amount: self.unbonded_amount,
149            asset_id: VotingReceiptToken::new(self.proposal).id(),
150        }
151        .into()
152    }
153}
154
155impl From<DelegatorVotePlan> for pb::DelegatorVotePlan {
156    fn from(inner: DelegatorVotePlan) -> Self {
157        pb::DelegatorVotePlan {
158            proposal: inner.proposal,
159            vote: Some(inner.vote.into()),
160            start_position: inner.start_position.into(),
161            staked_note: Some(inner.staked_note.into()),
162            unbonded_amount: Some(inner.unbonded_amount.into()),
163            staked_note_position: inner.position.into(),
164            randomizer: inner.randomizer.to_bytes().to_vec(),
165            proof_blinding_r: inner.proof_blinding_r.to_bytes().to_vec(),
166            proof_blinding_s: inner.proof_blinding_s.to_bytes().to_vec(),
167        }
168    }
169}
170
171impl TryFrom<pb::DelegatorVotePlan> for DelegatorVotePlan {
172    type Error = anyhow::Error;
173
174    fn try_from(value: pb::DelegatorVotePlan) -> Result<Self, Self::Error> {
175        let proof_blinding_r_bytes: [u8; 32] = value
176            .proof_blinding_r
177            .try_into()
178            .map_err(|_| anyhow::anyhow!("malformed r in `DelegatorVotePlan`"))?;
179        let proof_blinding_s_bytes: [u8; 32] = value
180            .proof_blinding_s
181            .try_into()
182            .map_err(|_| anyhow::anyhow!("malformed s in `DelegatorVotePlan`"))?;
183
184        Ok(DelegatorVotePlan {
185            proposal: value.proposal,
186            start_position: value.start_position.into(),
187            vote: value
188                .vote
189                .ok_or_else(|| anyhow::anyhow!("missing vote in `DelegatorVotePlan`"))?
190                .try_into()?,
191            staked_note: value
192                .staked_note
193                .ok_or_else(|| anyhow::anyhow!("missing staked note in `DelegatorVotePlan`"))?
194                .try_into()?,
195            unbonded_amount: value
196                .unbonded_amount
197                .ok_or_else(|| anyhow::anyhow!("missing unbonded amount in `DelegatorVotePlan`"))?
198                .try_into()?,
199            position: value.staked_note_position.into(),
200            randomizer: Fr::from_bytes_checked(
201                value
202                    .randomizer
203                    .as_slice()
204                    .try_into()
205                    .map_err(|_| anyhow::anyhow!("invalid randomizer"))?,
206            )
207            .expect("randomizer malformed"),
208            proof_blinding_r: Fq::from_bytes_checked(&proof_blinding_r_bytes)
209                .expect("proof_blinding_r malformed"),
210            proof_blinding_s: Fq::from_bytes_checked(&proof_blinding_s_bytes)
211                .expect("proof_blinding_s malformed"),
212        })
213    }
214}
215
216impl DomainType for DelegatorVotePlan {
217    type Proto = pb::DelegatorVotePlan;
218}