penumbra_sdk_shielded_pool/spend/
plan.rs

1use decaf377::{Fq, Fr};
2use decaf377_rdsa::{Signature, SpendAuth};
3use penumbra_sdk_asset::{Balance, Value, STAKING_TOKEN_ASSET_ID};
4use penumbra_sdk_keys::{keys::AddressIndex, FullViewingKey};
5use penumbra_sdk_proto::{core::component::shielded_pool::v1 as pb, DomainType};
6use penumbra_sdk_sct::Nullifier;
7use penumbra_sdk_tct as tct;
8use rand_core::{CryptoRng, RngCore};
9use serde::{Deserialize, Serialize};
10
11use super::{Body, Spend, SpendProof};
12use crate::{Backref, Note, Rseed, SpendProofPrivate, SpendProofPublic};
13
14/// A planned [`Spend`](Spend).
15#[derive(Clone, Debug, Deserialize, Serialize)]
16#[serde(try_from = "pb::SpendPlan", into = "pb::SpendPlan")]
17pub struct SpendPlan {
18    pub note: Note,
19    pub position: tct::Position,
20    pub randomizer: Fr,
21    pub value_blinding: Fr,
22    pub proof_blinding_r: Fq,
23    pub proof_blinding_s: Fq,
24}
25
26impl SpendPlan {
27    /// Create a new [`SpendPlan`] that spends the given `position`ed `note`.
28    pub fn new<R: CryptoRng + RngCore>(
29        rng: &mut R,
30        note: Note,
31        position: tct::Position,
32    ) -> SpendPlan {
33        SpendPlan {
34            note,
35            position,
36            randomizer: Fr::rand(rng),
37            value_blinding: Fr::rand(rng),
38            proof_blinding_r: Fq::rand(rng),
39            proof_blinding_s: Fq::rand(rng),
40        }
41    }
42
43    /// Create a dummy [`SpendPlan`].
44    pub fn dummy<R: CryptoRng + RngCore>(rng: &mut R, fvk: &FullViewingKey) -> SpendPlan {
45        // A valid address we can spend; since the note is hidden, we can just pick the default.
46        let dummy_address = fvk.payment_address(AddressIndex::default()).0;
47        let rseed = Rseed::generate(rng);
48        let dummy_note = Note::from_parts(
49            dummy_address,
50            Value {
51                amount: 0u64.into(),
52                asset_id: *STAKING_TOKEN_ASSET_ID,
53            },
54            rseed,
55        )
56        .expect("dummy note is valid");
57
58        Self::new(rng, dummy_note, 0u64.into())
59    }
60
61    /// Convenience method to construct the [`Spend`] described by this [`SpendPlan`].
62    pub fn spend(
63        &self,
64        fvk: &FullViewingKey,
65        auth_sig: Signature<SpendAuth>,
66        auth_path: tct::Proof,
67        anchor: tct::Root,
68    ) -> Spend {
69        Spend {
70            body: self.spend_body(fvk),
71            auth_sig,
72            proof: self.spend_proof(fvk, auth_path, anchor),
73        }
74    }
75
76    /// Construct the [`spend::Body`] described by this [`SpendPlan`].
77    pub fn spend_body(&self, fvk: &FullViewingKey) -> Body {
78        // Construct the backreference for this spend.
79        let backref = Backref::new(self.note.commit());
80        let encrypted_backref = backref.encrypt(&fvk.backref_key(), &self.nullifier(fvk));
81        Body {
82            balance_commitment: self.balance().commit(self.value_blinding),
83            nullifier: self.nullifier(fvk),
84            rk: self.rk(fvk),
85            encrypted_backref,
86        }
87    }
88
89    /// Construct the randomized verification key associated with this [`SpendPlan`].
90    pub fn rk(&self, fvk: &FullViewingKey) -> decaf377_rdsa::VerificationKey<SpendAuth> {
91        fvk.spend_verification_key().randomize(&self.randomizer)
92    }
93
94    /// Construct the [`Nullifier`] associated with this [`SpendPlan`].
95    pub fn nullifier(&self, fvk: &FullViewingKey) -> Nullifier {
96        let nk = fvk.nullifier_key();
97        Nullifier::derive(nk, self.position, &self.note.commit())
98    }
99
100    /// Construct the [`SpendProof`] required by the [`spend::Body`] described by this [`SpendPlan`].
101    pub fn spend_proof(
102        &self,
103        fvk: &FullViewingKey,
104        state_commitment_proof: tct::Proof,
105        anchor: tct::Root,
106    ) -> SpendProof {
107        let public = SpendProofPublic {
108            anchor,
109            balance_commitment: self.balance().commit(self.value_blinding),
110            nullifier: self.nullifier(fvk),
111            rk: self.rk(fvk),
112        };
113        let private = SpendProofPrivate {
114            state_commitment_proof,
115            note: self.note.clone(),
116            v_blinding: self.value_blinding,
117            spend_auth_randomizer: self.randomizer,
118            ak: *fvk.spend_verification_key(),
119            nk: *fvk.nullifier_key(),
120        };
121        SpendProof::prove(
122            self.proof_blinding_r,
123            self.proof_blinding_s,
124            &penumbra_sdk_proof_params::SPEND_PROOF_PROVING_KEY,
125            public,
126            private,
127        )
128        .expect("can generate ZKSpendProof")
129    }
130
131    pub fn balance(&self) -> Balance {
132        Value {
133            amount: self.note.value().amount,
134            asset_id: self.note.value().asset_id,
135        }
136        .into()
137    }
138}
139
140impl DomainType for SpendPlan {
141    type Proto = pb::SpendPlan;
142}
143
144impl From<SpendPlan> for pb::SpendPlan {
145    fn from(msg: SpendPlan) -> Self {
146        Self {
147            note: Some(msg.note.into()),
148            position: u64::from(msg.position),
149            randomizer: msg.randomizer.to_bytes().to_vec(),
150            value_blinding: msg.value_blinding.to_bytes().to_vec(),
151            proof_blinding_r: msg.proof_blinding_r.to_bytes().to_vec(),
152            proof_blinding_s: msg.proof_blinding_s.to_bytes().to_vec(),
153        }
154    }
155}
156
157impl TryFrom<pb::SpendPlan> for SpendPlan {
158    type Error = anyhow::Error;
159    fn try_from(msg: pb::SpendPlan) -> Result<Self, Self::Error> {
160        Ok(Self {
161            note: msg
162                .note
163                .ok_or_else(|| anyhow::anyhow!("missing note"))?
164                .try_into()?,
165            position: msg.position.into(),
166            randomizer: Fr::from_bytes_checked(msg.randomizer.as_slice().try_into()?)
167                .expect("randomizer malformed"),
168            value_blinding: Fr::from_bytes_checked(msg.value_blinding.as_slice().try_into()?)
169                .expect("value_blinding malformed"),
170            proof_blinding_r: Fq::from_bytes_checked(msg.proof_blinding_r.as_slice().try_into()?)
171                .expect("proof_blinding_r malformed"),
172            proof_blinding_s: Fq::from_bytes_checked(msg.proof_blinding_s.as_slice().try_into()?)
173                .expect("proof_blinding_s malformed"),
174        })
175    }
176}