penumbra_sdk_shielded_pool/spend/
plan.rs1use 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#[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 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 pub fn dummy<R: CryptoRng + RngCore>(rng: &mut R, fvk: &FullViewingKey) -> SpendPlan {
45 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 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 pub fn spend_body(&self, fvk: &FullViewingKey) -> Body {
78 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 pub fn rk(&self, fvk: &FullViewingKey) -> decaf377_rdsa::VerificationKey<SpendAuth> {
91 fvk.spend_verification_key().randomize(&self.randomizer)
92 }
93
94 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 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}