penumbra_sdk_shielded_pool/output/
plan.rs1use decaf377::{Fq, Fr};
2use decaf377_ka as ka;
3use penumbra_sdk_asset::{Balance, Value, STAKING_TOKEN_ASSET_ID};
4use penumbra_sdk_keys::{
5 keys::{IncomingViewingKey, OutgoingViewingKey},
6 symmetric::WrappedMemoKey,
7 Address, PayloadKey,
8};
9use penumbra_sdk_proto::{core::component::shielded_pool::v1 as pb, DomainType};
10use rand_core::{CryptoRng, RngCore};
11use serde::{Deserialize, Serialize};
12
13use super::{Body, Output, OutputProof, OutputProofPrivate, OutputProofPublic};
14use crate::{Note, Rseed};
15
16#[derive(Clone, Debug, Deserialize, Serialize)]
18#[serde(try_from = "pb::OutputPlan", into = "pb::OutputPlan")]
19pub struct OutputPlan {
20 pub value: Value,
21 pub dest_address: Address,
22 pub rseed: Rseed,
23 pub value_blinding: Fr,
24 pub proof_blinding_r: Fq,
25 pub proof_blinding_s: Fq,
26}
27
28impl OutputPlan {
29 pub fn new<R: RngCore + CryptoRng>(
31 rng: &mut R,
32 value: Value,
33 dest_address: Address,
34 ) -> OutputPlan {
35 let rseed = Rseed::generate(rng);
36 let value_blinding = Fr::rand(rng);
37 Self {
38 value,
39 dest_address,
40 rseed,
41 value_blinding,
42 proof_blinding_r: Fq::rand(rng),
43 proof_blinding_s: Fq::rand(rng),
44 }
45 }
46
47 pub fn dummy<R: CryptoRng + RngCore>(rng: &mut R) -> OutputPlan {
49 let dummy_address = Address::dummy(rng);
50 Self::new(
51 rng,
52 Value {
53 amount: 0u64.into(),
54 asset_id: *STAKING_TOKEN_ASSET_ID,
55 },
56 dummy_address,
57 )
58 }
59
60 pub fn output(&self, ovk: &OutgoingViewingKey, memo_key: &PayloadKey) -> Output {
63 Output {
64 body: self.output_body(ovk, memo_key),
65 proof: self.output_proof(),
66 }
67 }
68
69 pub fn output_note(&self) -> Note {
70 Note::from_parts(self.dest_address.clone(), self.value, self.rseed)
71 .expect("transmission key in address is always valid")
72 }
73
74 pub fn output_proof(&self) -> OutputProof {
77 let note = self.output_note();
78 let balance_commitment = self.balance().commit(self.value_blinding);
79 let note_commitment = note.commit();
80 OutputProof::prove(
81 self.proof_blinding_r,
82 self.proof_blinding_s,
83 &penumbra_sdk_proof_params::OUTPUT_PROOF_PROVING_KEY,
84 OutputProofPublic {
85 balance_commitment,
86 note_commitment,
87 },
88 OutputProofPrivate {
89 note,
90 balance_blinding: self.value_blinding,
91 },
92 )
93 .expect("can generate ZKOutputProof")
94 }
95
96 pub fn output_body(&self, ovk: &OutgoingViewingKey, memo_key: &PayloadKey) -> Body {
98 let note = self.output_note();
100 let balance_commitment = self.balance().commit(self.value_blinding);
101
102 let esk: ka::Secret = note.ephemeral_secret_key();
104 let ovk_wrapped_key = note.encrypt_key(ovk, balance_commitment);
106
107 let wrapped_memo_key = WrappedMemoKey::encrypt(
108 memo_key,
109 esk,
110 note.transmission_key(),
111 ¬e.diversified_generator(),
112 );
113
114 Body {
115 note_payload: note.payload(),
116 balance_commitment,
117 ovk_wrapped_key,
118 wrapped_memo_key,
119 }
120 }
121
122 pub fn is_viewed_by(&self, ivk: &IncomingViewingKey) -> bool {
124 ivk.views_address(&self.dest_address)
125 }
126
127 pub fn balance(&self) -> Balance {
128 -Balance::from(self.value)
129 }
130}
131
132impl DomainType for OutputPlan {
133 type Proto = pb::OutputPlan;
134}
135
136impl From<OutputPlan> for pb::OutputPlan {
137 fn from(msg: OutputPlan) -> Self {
138 Self {
139 value: Some(msg.value.into()),
140 dest_address: Some(msg.dest_address.into()),
141 rseed: msg.rseed.to_bytes().to_vec(),
142 value_blinding: msg.value_blinding.to_bytes().to_vec(),
143 proof_blinding_r: msg.proof_blinding_r.to_bytes().to_vec(),
144 proof_blinding_s: msg.proof_blinding_s.to_bytes().to_vec(),
145 }
146 }
147}
148
149impl TryFrom<pb::OutputPlan> for OutputPlan {
150 type Error = anyhow::Error;
151 fn try_from(msg: pb::OutputPlan) -> Result<Self, Self::Error> {
152 Ok(Self {
153 value: msg
154 .value
155 .ok_or_else(|| anyhow::anyhow!("missing value"))?
156 .try_into()?,
157 dest_address: msg
158 .dest_address
159 .ok_or_else(|| anyhow::anyhow!("missing address"))?
160 .try_into()?,
161 rseed: Rseed(msg.rseed.as_slice().try_into()?),
162 value_blinding: Fr::from_bytes_checked(msg.value_blinding.as_slice().try_into()?)
163 .expect("value_blinding malformed"),
164 proof_blinding_r: Fq::from_bytes_checked(msg.proof_blinding_r.as_slice().try_into()?)
165 .expect("proof_blinding_r malformed"),
166 proof_blinding_s: Fq::from_bytes_checked(msg.proof_blinding_s.as_slice().try_into()?)
167 .expect("proof_blinding_s malformed"),
168 })
169 }
170}
171
172#[cfg(test)]
173mod test {
174 use crate::output::OutputProofPublic;
175
176 use super::OutputPlan;
177 use penumbra_sdk_asset::Value;
178 use penumbra_sdk_keys::{
179 keys::{Bip44Path, SeedPhrase, SpendKey},
180 PayloadKey,
181 };
182 use penumbra_sdk_proof_params::OUTPUT_PROOF_VERIFICATION_KEY;
183 use rand_core::OsRng;
184
185 #[test]
186 fn check_output_proof_verification() {
190 let mut rng = OsRng;
191 let seed_phrase = SeedPhrase::generate(rng);
192 let sk = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
193 let ovk = sk.full_viewing_key().outgoing();
194 let dummy_memo_key: PayloadKey = [0; 32].into();
195
196 let value: Value = "1234.02penumbra".parse().unwrap();
197 let dest_address = "penumbra1rqcd3hfvkvc04c4c9vc0ac87lh4y0z8l28k4xp6d0cnd5jc6f6k0neuzp6zdwtpwyfpswtdzv9jzqtpjn5t6wh96pfx3flq2dhqgc42u7c06kj57dl39w2xm6tg0wh4zc8kjjk".parse().unwrap();
198
199 let output_plan = OutputPlan::new(&mut rng, value, dest_address);
200 let blinding_factor = output_plan.value_blinding;
201
202 let _body = output_plan.output_body(ovk, &dummy_memo_key);
203
204 let balance_commitment = output_plan.balance().commit(blinding_factor);
205 let note_commitment = output_plan.output_note().commit();
206 let output_proof = output_plan.output_proof();
207
208 output_proof
209 .verify(
210 &OUTPUT_PROOF_VERIFICATION_KEY,
211 OutputProofPublic {
212 balance_commitment,
213 note_commitment,
214 },
215 )
216 .unwrap();
217 }
218}