penumbra_sdk_dex/swap/
plan.rs

1use anyhow::{anyhow, Context, Result};
2use ark_ff::Zero;
3
4use decaf377::{Fq, Fr};
5use penumbra_sdk_asset::{balance, Balance, Value};
6use penumbra_sdk_keys::FullViewingKey;
7use penumbra_sdk_proto::{penumbra::core::component::dex::v1 as pb, DomainType};
8use rand_core::{CryptoRng, RngCore};
9use serde::{Deserialize, Serialize};
10
11use crate::swap::proof::{SwapProofPrivate, SwapProofPublic};
12
13// TODO: rename action::Body to SwapBody
14use super::{action as swap, proof::SwapProof, Swap, SwapPlaintext};
15
16/// A planned [`Swap`](Swap).
17#[derive(Clone, Debug, Deserialize, Serialize)]
18#[serde(try_from = "pb::SwapPlan", into = "pb::SwapPlan")]
19pub struct SwapPlan {
20    pub swap_plaintext: SwapPlaintext,
21    pub fee_blinding: Fr,
22    pub proof_blinding_r: Fq,
23    pub proof_blinding_s: Fq,
24}
25
26impl SwapPlan {
27    /// Create a new [`SwapPlan`] that requests a swap between the given assets and input amounts.
28    pub fn new<R: CryptoRng + RngCore>(rng: &mut R, swap_plaintext: SwapPlaintext) -> SwapPlan {
29        let fee_blinding = Fr::rand(rng);
30
31        SwapPlan {
32            fee_blinding,
33            swap_plaintext,
34            proof_blinding_r: Fq::rand(rng),
35            proof_blinding_s: Fq::rand(rng),
36        }
37    }
38
39    /// Convenience method to construct the [`Swap`] described by this [`SwapPlan`].
40    pub fn swap(&self, fvk: &FullViewingKey) -> Swap {
41        Swap {
42            body: self.swap_body(fvk),
43            proof: self.swap_proof(),
44        }
45    }
46
47    /// Construct the [`swap::Body`] described by this [`SwapPlan`].
48    pub fn swap_body(&self, fvk: &FullViewingKey) -> swap::Body {
49        swap::Body {
50            trading_pair: self.swap_plaintext.trading_pair,
51            delta_1_i: self.swap_plaintext.delta_1_i,
52            delta_2_i: self.swap_plaintext.delta_2_i,
53            fee_commitment: self.fee_commitment(),
54            payload: self.swap_plaintext.encrypt(fvk.outgoing()),
55        }
56    }
57
58    /// Construct the [`SwapProof`] required by the [`swap::Body`] described by this [`SwapPlan`].
59    pub fn swap_proof(&self) -> SwapProof {
60        use penumbra_sdk_proof_params::SWAP_PROOF_PROVING_KEY;
61
62        let balance_commitment =
63            self.transparent_balance().commit(Fr::zero()) + self.fee_commitment();
64        SwapProof::prove(
65            self.proof_blinding_r,
66            self.proof_blinding_s,
67            &SWAP_PROOF_PROVING_KEY,
68            SwapProofPublic {
69                balance_commitment,
70                swap_commitment: self.swap_plaintext.swap_commitment(),
71                fee_commitment: self.fee_commitment(),
72            },
73            SwapProofPrivate {
74                fee_blinding: self.fee_blinding,
75                swap_plaintext: self.swap_plaintext.clone(),
76            },
77        )
78        .expect("can generate ZKSwapProof")
79    }
80
81    pub fn fee_commitment(&self) -> balance::Commitment {
82        self.swap_plaintext.claim_fee.commit(self.fee_blinding)
83    }
84
85    pub fn transparent_balance(&self) -> Balance {
86        let value_1 = Value {
87            amount: self.swap_plaintext.delta_1_i,
88            asset_id: self.swap_plaintext.trading_pair.asset_1(),
89        };
90        let value_2 = Value {
91            amount: self.swap_plaintext.delta_2_i,
92            asset_id: self.swap_plaintext.trading_pair.asset_2(),
93        };
94
95        let mut balance = Balance::default();
96        balance -= value_1;
97        balance -= value_2;
98        balance
99    }
100
101    pub fn balance(&self) -> Balance {
102        // Swaps must have spends corresponding to:
103        // - the input amount of asset 1
104        // - the input amount of asset 2
105        // - the pre-paid swap claim fee
106        let value_fee = Value {
107            amount: self.swap_plaintext.claim_fee.amount(),
108            asset_id: self.swap_plaintext.claim_fee.asset_id(),
109        };
110
111        let mut balance = self.transparent_balance();
112        balance -= value_fee;
113        balance
114    }
115}
116
117impl DomainType for SwapPlan {
118    type Proto = pb::SwapPlan;
119}
120
121impl From<SwapPlan> for pb::SwapPlan {
122    fn from(msg: SwapPlan) -> Self {
123        Self {
124            swap_plaintext: Some(msg.swap_plaintext.into()),
125            fee_blinding: msg.fee_blinding.to_bytes().to_vec(),
126            proof_blinding_r: msg.proof_blinding_r.to_bytes().to_vec(),
127            proof_blinding_s: msg.proof_blinding_s.to_bytes().to_vec(),
128        }
129    }
130}
131
132impl TryFrom<pb::SwapPlan> for SwapPlan {
133    type Error = anyhow::Error;
134    fn try_from(msg: pb::SwapPlan) -> Result<Self, Self::Error> {
135        let proof_blinding_r_bytes: [u8; 32] = msg
136            .proof_blinding_r
137            .try_into()
138            .map_err(|_| anyhow::anyhow!("malformed r in `SwapPlan`"))?;
139        let proof_blinding_s_bytes: [u8; 32] = msg
140            .proof_blinding_s
141            .try_into()
142            .map_err(|_| anyhow::anyhow!("malformed s in `SwapPlan`"))?;
143
144        let fee_blinding_bytes: [u8; 32] = msg.fee_blinding[..]
145            .try_into()
146            .map_err(|_| anyhow!("expected 32 byte fee blinding"))?;
147        Ok(Self {
148            fee_blinding: Fr::from_bytes_checked(&fee_blinding_bytes)
149                .expect("fee blinding malformed"),
150            swap_plaintext: msg
151                .swap_plaintext
152                .ok_or_else(|| anyhow!("missing swap_plaintext"))?
153                .try_into()
154                .context("swap plaintext malformed")?,
155            proof_blinding_r: Fq::from_bytes_checked(&proof_blinding_r_bytes)
156                .expect("proof_blinding_r malformed"),
157            proof_blinding_s: Fq::from_bytes_checked(&proof_blinding_s_bytes)
158                .expect("proof_blinding_r malformed"),
159        })
160    }
161}