penumbra_sdk_dex/swap_claim/
plan.rs

1use decaf377::Fq;
2use penumbra_sdk_asset::{Balance, Value};
3use penumbra_sdk_keys::{keys::IncomingViewingKey, FullViewingKey};
4use penumbra_sdk_proof_params::SWAPCLAIM_PROOF_PROVING_KEY;
5use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
6use penumbra_sdk_sct::Nullifier;
7use penumbra_sdk_tct as tct;
8
9use serde::{Deserialize, Serialize};
10use tct::Position;
11
12use crate::{swap::SwapPlaintext, BatchSwapOutputData};
13
14use super::{
15    action as swap_claim,
16    proof::{SwapClaimProof, SwapClaimProofPrivate, SwapClaimProofPublic},
17    SwapClaim,
18};
19
20/// A planned [`SwapClaim`](SwapClaim).
21#[derive(Clone, Debug, Deserialize, Serialize)]
22#[serde(try_from = "pb::SwapClaimPlan", into = "pb::SwapClaimPlan")]
23pub struct SwapClaimPlan {
24    pub swap_plaintext: SwapPlaintext,
25    pub position: Position,
26    pub output_data: BatchSwapOutputData,
27    pub epoch_duration: u64,
28    /// The first blinding factor used for generating the ZK proof.
29    pub proof_blinding_r: Fq,
30    /// The second blinding factor used for generating the ZK proof.
31    pub proof_blinding_s: Fq,
32}
33
34impl SwapClaimPlan {
35    /// Convenience method to construct the [`SwapClaim`] described by this
36    /// [`SwapClaimPlan`].
37    pub fn swap_claim(
38        &self,
39        fvk: &FullViewingKey,
40        state_commitment_proof: &tct::Proof,
41    ) -> SwapClaim {
42        SwapClaim {
43            body: self.swap_claim_body(fvk),
44            proof: self.swap_claim_proof(state_commitment_proof, fvk),
45            epoch_duration: self.epoch_duration,
46        }
47    }
48
49    /// Construct the [`SwapClaimProof`] required by the [`swap_claim::Body`] described
50    /// by this plan.
51    pub fn swap_claim_proof(
52        &self,
53        state_commitment_proof: &tct::Proof,
54        fvk: &FullViewingKey,
55    ) -> SwapClaimProof {
56        let (lambda_1, lambda_2) = self
57            .output_data
58            .pro_rata_outputs((self.swap_plaintext.delta_1_i, self.swap_plaintext.delta_2_i));
59        let (output_rseed_1, output_rseed_2) = self.swap_plaintext.output_rseeds();
60        let note_blinding_1 = output_rseed_1.derive_note_blinding();
61        let note_blinding_2 = output_rseed_2.derive_note_blinding();
62        let (output_1_note, output_2_note) = self.swap_plaintext.output_notes(&self.output_data);
63        let note_commitment_1 = output_1_note.commit();
64        let note_commitment_2 = output_2_note.commit();
65
66        let nullifier = Nullifier::derive(
67            fvk.nullifier_key(),
68            self.position,
69            &self.swap_plaintext.swap_commitment(),
70        );
71        SwapClaimProof::prove(
72            self.proof_blinding_r,
73            self.proof_blinding_s,
74            &SWAPCLAIM_PROOF_PROVING_KEY,
75            SwapClaimProofPublic {
76                anchor: state_commitment_proof.root(),
77                nullifier,
78                claim_fee: self.swap_plaintext.claim_fee.clone(),
79                output_data: self.output_data,
80                note_commitment_1,
81                note_commitment_2,
82            },
83            SwapClaimProofPrivate {
84                swap_plaintext: self.swap_plaintext.clone(),
85                state_commitment_proof: state_commitment_proof.clone(),
86                nk: *fvk.nullifier_key(),
87                ak: *fvk.spend_verification_key(),
88                lambda_1,
89                lambda_2,
90                note_blinding_1,
91                note_blinding_2,
92            },
93        )
94        .expect("can generate ZKSwapClaimProof")
95    }
96
97    /// Construct the [`swap_claim::Body`] described by this plan.
98    pub fn swap_claim_body(&self, fvk: &FullViewingKey) -> swap_claim::Body {
99        let (output_1_note, output_2_note) = self.swap_plaintext.output_notes(&self.output_data);
100        tracing::debug!(?output_1_note, ?output_2_note);
101
102        // We need to get the correct diversified generator to use with DH:
103        let output_1_commitment = output_1_note.commit();
104        let output_2_commitment = output_2_note.commit();
105
106        let nullifier = Nullifier::derive(
107            fvk.nullifier_key(),
108            self.position,
109            &self.swap_plaintext.swap_commitment(),
110        );
111
112        swap_claim::Body {
113            nullifier,
114            fee: self.swap_plaintext.claim_fee.clone(),
115            output_1_commitment,
116            output_2_commitment,
117            output_data: self.output_data,
118        }
119    }
120
121    /// Checks whether this plan's output is viewed by the given IVK.
122    pub fn is_viewed_by(&self, ivk: &IncomingViewingKey) -> bool {
123        ivk.views_address(&self.swap_plaintext.claim_address)
124    }
125
126    pub fn balance(&self) -> Balance {
127        // Only the pre-paid fee is contributed to the value balance
128        // The rest is handled internally to the SwapClaim action.
129        let value_fee = Value {
130            amount: self.swap_plaintext.claim_fee.amount(),
131            asset_id: self.swap_plaintext.claim_fee.asset_id(),
132        };
133
134        value_fee.into()
135    }
136}
137
138impl DomainType for SwapClaimPlan {
139    type Proto = pb::SwapClaimPlan;
140}
141
142impl From<SwapClaimPlan> for pb::SwapClaimPlan {
143    fn from(msg: SwapClaimPlan) -> Self {
144        Self {
145            swap_plaintext: Some(msg.swap_plaintext.into()),
146            position: msg.position.into(),
147            output_data: Some(msg.output_data.into()),
148            epoch_duration: msg.epoch_duration,
149            proof_blinding_r: msg.proof_blinding_r.to_bytes().to_vec(),
150            proof_blinding_s: msg.proof_blinding_s.to_bytes().to_vec(),
151        }
152    }
153}
154
155impl TryFrom<pb::SwapClaimPlan> for SwapClaimPlan {
156    type Error = anyhow::Error;
157    fn try_from(msg: pb::SwapClaimPlan) -> Result<Self, Self::Error> {
158        let proof_blinding_r_bytes: [u8; 32] = msg
159            .proof_blinding_r
160            .try_into()
161            .map_err(|_| anyhow::anyhow!("malformed r in `SwapClaimPlan`"))?;
162        let proof_blinding_s_bytes: [u8; 32] = msg
163            .proof_blinding_s
164            .try_into()
165            .map_err(|_| anyhow::anyhow!("malformed s in `SwapClaimPlan`"))?;
166
167        Ok(Self {
168            swap_plaintext: msg
169                .swap_plaintext
170                .ok_or_else(|| anyhow::anyhow!("missing swap_plaintext"))?
171                .try_into()?,
172            position: msg.position.into(),
173            output_data: msg
174                .output_data
175                .ok_or_else(|| anyhow::anyhow!("missing output_data"))?
176                .try_into()?,
177            epoch_duration: msg.epoch_duration,
178            proof_blinding_r: Fq::from_bytes_checked(&proof_blinding_r_bytes)
179                .expect("proof_blinding_r malformed"),
180            proof_blinding_s: Fq::from_bytes_checked(&proof_blinding_s_bytes)
181                .expect("proof_blinding_s malformed"),
182        })
183    }
184}