penumbra_sdk_shielded_pool/
convert.rs1use anyhow::{anyhow, Result};
2use ark_ff::ToConstraintField;
3use ark_groth16::{
4 r1cs_to_qap::LibsnarkReduction, Groth16, PreparedVerifyingKey, Proof, ProvingKey,
5};
6use ark_relations::r1cs;
7use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
8use ark_snark::SNARK;
9use base64::prelude::*;
10use decaf377::{Bls12_377, Fq, Fr};
11use penumbra_sdk_asset::{
12 asset::{self, AssetIdVar},
13 balance::{self, commitment::BalanceCommitmentVar, BalanceVar},
14 Balance, Value, ValueVar, STAKING_TOKEN_ASSET_ID,
15};
16use penumbra_sdk_num::{
17 fixpoint::{U128x128, U128x128Var},
18 Amount, AmountVar,
19};
20use penumbra_sdk_proof_params::{DummyWitness, VerifyingKeyExt, GROTH16_PROOF_LENGTH_BYTES};
21
22#[derive(Clone, Debug)]
24pub struct ConvertProofPublic {
25 pub from: asset::Id,
27 pub to: asset::Id,
29 pub rate: U128x128,
31 pub balance_commitment: balance::Commitment,
33}
34
35#[derive(Clone, Debug)]
37pub struct ConvertProofPrivate {
38 pub amount: Amount,
40 pub balance_blinding: Fr,
42}
43
44#[cfg(test)]
45fn check_satisfaction(public: &ConvertProofPublic, private: &ConvertProofPrivate) -> Result<()> {
46 let consumed = Value {
47 amount: private.amount,
48 asset_id: public.from,
49 };
50 let produced = Value {
51 amount: public.rate.apply_to_amount(&private.amount)?,
52 asset_id: public.to,
53 };
54 let balance: Balance = Balance::from(produced) - consumed;
55 let commitment = balance.commit(private.balance_blinding);
56 if commitment != public.balance_commitment {
57 anyhow::bail!("balance commitment did not match public input");
58 }
59 Ok(())
60}
61
62#[cfg(test)]
63fn check_circuit_satisfaction(
64 public: ConvertProofPublic,
65 private: ConvertProofPrivate,
66) -> Result<()> {
67 use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystem};
68
69 let cs = ConstraintSystem::new_ref();
70 let circuit = ConvertCircuit::new(public, private);
71 cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
72 circuit
74 .generate_constraints(cs.clone())
75 .expect("can generate constraints from circuit");
76 cs.finalize();
77 if !cs.is_satisfied()? {
78 anyhow::bail!("constraints are not satisfied");
79 }
80 Ok(())
81}
82
83#[derive(Clone, Debug)]
85pub struct ConvertCircuit {
86 amount: Amount,
88 balance_blinding: Fr,
90 from: asset::Id,
92 to: asset::Id,
94 rate: U128x128,
96 balance_commitment: balance::Commitment,
98}
99
100impl ConvertCircuit {
101 fn new(
102 ConvertProofPublic {
103 from,
104 to,
105 rate,
106 balance_commitment,
107 }: ConvertProofPublic,
108 ConvertProofPrivate {
109 amount,
110 balance_blinding,
111 }: ConvertProofPrivate,
112 ) -> Self {
113 Self {
114 amount,
115 balance_blinding,
116 balance_commitment,
117 from,
118 to,
119 rate,
120 }
121 }
122}
123
124impl r1cs::ConstraintSynthesizer<Fq> for ConvertCircuit {
125 fn generate_constraints(self, cs: r1cs::ConstraintSystemRef<Fq>) -> r1cs::Result<()> {
126 use ark_r1cs_std::prelude::*;
127
128 let amount_var = AmountVar::new_witness(cs.clone(), || Ok(self.amount))?;
130 let balance_blinding_var = {
131 let balance_blinding_arr: [u8; 32] = self.balance_blinding.to_bytes();
132 UInt8::new_witness_vec(cs.clone(), &balance_blinding_arr)?
133 };
134
135 let from = AssetIdVar::new_input(cs.clone(), || Ok(self.from))?;
137 let to = AssetIdVar::new_input(cs.clone(), || Ok(self.to))?;
138 let rate = U128x128Var::new_input(cs.clone(), || Ok(self.rate))?;
139 let balance_commitment =
140 BalanceCommitmentVar::new_input(cs.clone(), || Ok(self.balance_commitment))?;
141
142 let expected_balance = {
144 let taken = BalanceVar::from_negative_value_var(ValueVar {
145 amount: amount_var.clone(),
146 asset_id: from,
147 });
148
149 let produced = BalanceVar::from_positive_value_var(ValueVar {
150 amount: rate.apply_to_amount(amount_var)?,
151 asset_id: to,
152 });
153
154 taken + produced
155 };
156 let expected_commitment = expected_balance.commit(balance_blinding_var)?;
157 expected_commitment.enforce_equal(&balance_commitment)?;
158
159 Ok(())
160 }
161}
162
163impl DummyWitness for ConvertCircuit {
164 fn with_dummy_witness() -> Self {
165 let amount = Amount::from(1u64);
166 let balance_blinding = Fr::from(1u64);
167 let from = *STAKING_TOKEN_ASSET_ID;
168 let to = *STAKING_TOKEN_ASSET_ID;
169 let rate = U128x128::from(1u64);
170 let balance = Balance::from(Value {
171 asset_id: to,
172 amount,
173 }) - Balance::from(Value {
174 asset_id: from,
175 amount,
176 });
177 let balance_commitment = balance.commit(balance_blinding);
178 Self {
179 amount,
180 balance_blinding,
181 from,
182 to,
183 rate,
184 balance_commitment,
185 }
186 }
187}
188
189#[derive(Clone, Debug)]
194pub struct ConvertProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
195
196impl ConvertProof {
197 pub fn prove(
199 blinding_r: Fq,
200 blinding_s: Fq,
201 pk: &ProvingKey<Bls12_377>,
202 public: ConvertProofPublic,
203 private: ConvertProofPrivate,
204 ) -> Result<Self> {
205 let circuit = ConvertCircuit::new(public, private);
206 let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
207 circuit, pk, blinding_r, blinding_s,
208 )?;
209 let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
210 Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
211 Ok(Self(proof_bytes))
212 }
213
214 #[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(&self.0), vk = ?vk.debug_id()))]
215 pub fn verify(
216 &self,
217 vk: &PreparedVerifyingKey<Bls12_377>,
218 public: ConvertProofPublic,
219 ) -> Result<()> {
220 let proof = Proof::deserialize_compressed_unchecked(&self.0[..]).map_err(|e| anyhow!(e))?;
221
222 let mut public_inputs = Vec::new();
223 public_inputs.extend(
224 public
225 .from
226 .to_field_elements()
227 .ok_or_else(|| anyhow!("could not convert `from` asset ID to field elements"))?,
228 );
229 public_inputs.extend(
230 public
231 .to
232 .to_field_elements()
233 .ok_or_else(|| anyhow!("could not convert `to` asset ID to field elements"))?,
234 );
235 public_inputs.extend(
236 public
237 .rate
238 .to_field_elements()
239 .ok_or_else(|| anyhow!("could not convert exchange rate to field elements"))?,
240 );
241 public_inputs.extend(
242 public
243 .balance_commitment
244 .0
245 .to_field_elements()
246 .ok_or_else(|| anyhow!("could not convert balance commitment to field elements"))?,
247 );
248
249 tracing::trace!(?public_inputs);
250 let start = std::time::Instant::now();
251 let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
252 vk,
253 public_inputs.as_slice(),
254 &proof,
255 )?;
256 tracing::debug!(?proof_result, elapsed = ?start.elapsed());
257 proof_result
258 .then_some(())
259 .ok_or_else(|| anyhow!("undelegate claim proof did not verify"))
260 }
261
262 pub fn to_vec(&self) -> Vec<u8> {
263 self.0.to_vec()
264 }
265}
266
267impl TryFrom<&[u8]> for ConvertProof {
268 type Error = anyhow::Error;
269
270 fn try_from(value: &[u8]) -> Result<Self> {
271 Ok(Self(value.try_into()?))
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use proptest::prelude::*;
279
280 prop_compose! {
281 fn arb_valid_convert_statement(balance_blinding: Fr)(amount in any::<u64>(), from_asset_id64 in any::<u64>(), to_asset_id64 in any::<u64>(), rate in any::<(u64, u128)>()) -> (ConvertProofPublic, ConvertProofPrivate) {
282 let rate = U128x128::ratio(u128::from(rate.0), rate.1).expect("the bounds will make this not overflow");
283 let from = asset::Id(Fq::from(from_asset_id64));
284 let to = asset::Id(Fq::from(to_asset_id64));
285 let amount = Amount::from(amount);
286 let balance = Balance::from(Value { asset_id: to, amount: rate.apply_to_amount(&amount).expect("the bounds will make this not overflow")}) - Value {asset_id: from, amount};
287 let public = ConvertProofPublic { from, to, rate, balance_commitment: balance.commit(balance_blinding) };
288 let private = ConvertProofPrivate { amount, balance_blinding };
289 (public, private)
290 }
291 }
292
293 proptest! {
294 #[test]
295 fn convert_proof_happy_path((public, private) in arb_valid_convert_statement(Fr::from(1u64))) {
296 assert!(check_satisfaction(&public, &private).is_ok());
297 assert!(check_circuit_satisfaction(public, private).is_ok());
298 }
299 }
300
301 fn nonzero_u128() -> impl Strategy<Value = u128> {
302 prop::num::u128::ANY.prop_filter("nonzero", |x| *x != 0)
303 }
304
305 fn nonzero_u64() -> impl Strategy<Value = u64> {
306 prop::num::u64::ANY.prop_filter("nonzero", |x| *x != 0)
307 }
308
309 prop_compose! {
310 fn arb_invalid_convert_statement_wrong_rate(balance_blinding: Fr)(amount in any::<u64>(), from_asset_id64 in any::<u64>(), to_asset_id64 in any::<u64>(), rate_num in nonzero_u64(), rate_denom in nonzero_u128(), random_rate_num in nonzero_u64()) -> (ConvertProofPublic, ConvertProofPrivate) {
316 let rate = U128x128::ratio(u128::from(rate_num), rate_denom).expect("the bounds will make this not overflow");
317 let incorrect_rate = rate.checked_add(&U128x128::ratio(random_rate_num, 1u64).expect("should not overflow")).expect("should not overflow");
318 let from = asset::Id(Fq::from(from_asset_id64));
319 let to = asset::Id(Fq::from(to_asset_id64));
320 let amount = Amount::from(amount);
321 let balance = Balance::from(Value { asset_id: to, amount: incorrect_rate.apply_to_amount(&amount).expect("the bounds will make this not overflow")}) - Value {asset_id: from, amount};
322 let public = ConvertProofPublic { from, to, rate, balance_commitment: balance.commit(balance_blinding) };
323 let private = ConvertProofPrivate { amount, balance_blinding };
324 (public, private)
325 }
326 }
327
328 proptest! {
329 #[test]
330 fn convert_proof_invalid_convert_statement_wrong_rate((public, private) in arb_invalid_convert_statement_wrong_rate(Fr::from(1u64))) {
331 assert!(check_satisfaction(&public, &private).is_err());
332 assert!(check_circuit_satisfaction(public, private).is_err());
333 }
334 }
335}