penumbra_sdk_shielded_pool/output/
plan.rs

1use 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/// A planned [`Output`](Output).
17#[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    /// Create a new [`OutputPlan`] that sends `value` to `dest_address`.
30    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    /// Create a dummy [`OutputPlan`].
48    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    /// Convenience method to construct the [`Output`] described by this
61    /// [`OutputPlan`].
62    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    /// Construct the [`OutputProof`] required by the [`output::Body`] described
75    /// by this plan.
76    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    /// Construct the [`output::Body`] described by this plan.
97    pub fn output_body(&self, ovk: &OutgoingViewingKey, memo_key: &PayloadKey) -> Body {
98        // Prepare the output note and commitment.
99        let note = self.output_note();
100        let balance_commitment = self.balance().commit(self.value_blinding);
101
102        // Encrypt the note to the recipient...
103        let esk: ka::Secret = note.ephemeral_secret_key();
104        // ... and wrap the encryption key to ourselves.
105        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            &note.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    /// Checks whether this plan's output is viewed by the given IVK.
123    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    /// Check that a valid output proof passes the `penumbra_sdk_crypto` integrity checks successfully.
187    /// This test serves to anchor how an `OutputPlan` prepares its `OutputProof`, in particular
188    /// the balance and note commitments.
189    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}