1use anyhow::{Context, Result};
2use ark_ff::ToConstraintField;
3use ark_groth16::{
4 r1cs_to_qap::LibsnarkReduction, Groth16, PreparedVerifyingKey, Proof, ProvingKey,
5};
6use ark_r1cs_std::prelude::*;
7use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef};
8use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
9use ark_snark::SNARK;
10use decaf377::Bls12_377;
11use decaf377::{Fq, Fr};
12use decaf377_fmd as fmd;
13use decaf377_ka as ka;
14use penumbra_sdk_fee::Fee;
15use penumbra_sdk_proto::{core::component::dex::v1 as pb, DomainType};
16use penumbra_sdk_tct as tct;
17use penumbra_sdk_tct::r1cs::StateCommitmentVar;
18
19use penumbra_sdk_asset::{
20 asset,
21 balance::{self, commitment::BalanceCommitmentVar, BalanceVar},
22 Value,
23};
24use penumbra_sdk_keys::{keys::Diversifier, Address};
25use penumbra_sdk_shielded_pool::Rseed;
26
27use crate::{
28 swap::{SwapPlaintext, SwapPlaintextVar},
29 TradingPair,
30};
31
32use penumbra_sdk_proof_params::{DummyWitness, GROTH16_PROOF_LENGTH_BYTES};
33
34#[derive(Clone, Debug)]
36pub struct SwapProofPublic {
37 pub balance_commitment: balance::Commitment,
39 pub swap_commitment: tct::StateCommitment,
41 pub fee_commitment: balance::Commitment,
43}
44
45#[derive(Clone, Debug)]
47pub struct SwapProofPrivate {
48 pub fee_blinding: Fr,
50 pub swap_plaintext: SwapPlaintext,
52}
53
54#[cfg(test)]
55fn check_satisfaction(public: &SwapProofPublic, private: &SwapProofPrivate) -> Result<()> {
56 use penumbra_sdk_asset::Balance;
57
58 let swap_commitment = private.swap_plaintext.swap_commitment();
59 if swap_commitment != public.swap_commitment {
60 anyhow::bail!("swap commitment did not match public input");
61 }
62
63 let fee_balance = -Balance::from(private.swap_plaintext.claim_fee.0);
64 let fee_commitment = fee_balance.commit(private.fee_blinding);
65 if fee_commitment != public.fee_commitment {
66 anyhow::bail!("fee commitment did not match public input");
67 }
68
69 let balance_1 = -Balance::from(private.swap_plaintext.delta_1_value());
70 let balance_2 = -Balance::from(private.swap_plaintext.delta_2_value());
71 let transparent_blinding = Fr::from(0u64);
72 let balance_1_commit = balance_1.commit(transparent_blinding);
73 let balance_2_commit = balance_2.commit(transparent_blinding);
74 let transparent_balance_commitment = balance_1_commit + balance_2_commit;
75 let total_balance_commitment = transparent_balance_commitment + fee_commitment;
76 if total_balance_commitment != public.balance_commitment {
77 anyhow::bail!("balance commitment did not match public input");
78 }
79
80 Ok(())
81}
82
83#[cfg(test)]
84fn check_circuit_satisfaction(public: SwapProofPublic, private: SwapProofPrivate) -> Result<()> {
85 use ark_relations::r1cs::{self, ConstraintSystem};
86
87 let cs: ConstraintSystemRef<_> = ConstraintSystem::new_ref();
88 let circuit = SwapCircuit { public, private };
89 cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
90 circuit
91 .generate_constraints(cs.clone())
92 .expect("can generate constraints from circuit");
93 cs.finalize();
94 if !cs.is_satisfied()? {
95 anyhow::bail!("constraints are not satisfied");
96 }
97 Ok(())
98}
99
100pub struct SwapCircuit {
101 public: SwapProofPublic,
102 private: SwapProofPrivate,
103}
104
105impl ConstraintSynthesizer<Fq> for SwapCircuit {
106 fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
107 let swap_plaintext_var =
109 SwapPlaintextVar::new_witness(cs.clone(), || Ok(self.private.swap_plaintext.clone()))?;
110 let fee_blinding_var =
111 UInt8::new_witness_vec(cs.clone(), &self.private.fee_blinding.to_bytes())?;
112
113 let claimed_balance_commitment =
115 BalanceCommitmentVar::new_input(cs.clone(), || Ok(self.public.balance_commitment))?;
116 let claimed_swap_commitment =
117 StateCommitmentVar::new_input(cs.clone(), || Ok(self.public.swap_commitment))?;
118 let claimed_fee_commitment =
119 BalanceCommitmentVar::new_input(cs, || Ok(self.public.fee_commitment))?;
120
121 let swap_commitment = swap_plaintext_var.commit()?;
123 claimed_swap_commitment.enforce_equal(&swap_commitment)?;
124
125 let fee_balance = BalanceVar::from_negative_value_var(swap_plaintext_var.claim_fee.clone());
127 let fee_commitment = fee_balance.commit(fee_blinding_var)?;
128 claimed_fee_commitment.enforce_equal(&fee_commitment)?;
129
130 let transparent_blinding_var = UInt8::constant_vec(&[0u8; 32]);
132 let balance_1 = BalanceVar::from_negative_value_var(swap_plaintext_var.delta_1_value());
133 let balance_2 = BalanceVar::from_negative_value_var(swap_plaintext_var.delta_2_value());
134 let balance_1_commit = balance_1.commit(transparent_blinding_var.clone())?;
135 let balance_2_commit = balance_2.commit(transparent_blinding_var)?;
136 let transparent_balance_commitment = balance_1_commit + balance_2_commit;
137 let total_balance_commitment = transparent_balance_commitment + fee_commitment;
138
139 claimed_balance_commitment.enforce_equal(&total_balance_commitment)?;
141
142 Ok(())
143 }
144}
145
146impl DummyWitness for SwapCircuit {
147 fn with_dummy_witness() -> Self {
148 let a = asset::Cache::with_known_assets()
149 .get_unit("upenumbra")
150 .expect("upenumbra asset exists");
151 let b = asset::Cache::with_known_assets()
152 .get_unit("nala")
153 .expect("nala asset exists");
154 let trading_pair = TradingPair::new(a.id(), b.id());
155 let diversifier_bytes = [1u8; 16];
156 let pk_d_bytes = decaf377::Element::GENERATOR.vartime_compress().0;
157 let clue_key_bytes = [1; 32];
158 let diversifier = Diversifier(diversifier_bytes);
159 let address = Address::from_components(
160 diversifier,
161 ka::Public(pk_d_bytes),
162 fmd::ClueKey(clue_key_bytes),
163 )
164 .expect("generated 1 address");
165 let swap_plaintext = SwapPlaintext {
166 trading_pair,
167 delta_1_i: 100000u64.into(),
168 delta_2_i: 1u64.into(),
169 claim_fee: Fee(Value {
170 amount: 3u64.into(),
171 asset_id: asset::Cache::with_known_assets()
172 .get_unit("upenumbra")
173 .expect("upenumbra asset exists")
174 .id(),
175 }),
176 claim_address: address,
177 rseed: Rseed([1u8; 32]),
178 };
179
180 Self {
181 private: SwapProofPrivate {
182 swap_plaintext: swap_plaintext.clone(),
183 fee_blinding: Fr::from(1u64),
184 },
185 public: SwapProofPublic {
186 swap_commitment: swap_plaintext.swap_commitment(),
187 fee_commitment: balance::Commitment(decaf377::Element::GENERATOR),
188 balance_commitment: balance::Commitment(decaf377::Element::GENERATOR),
189 },
190 }
191 }
192}
193
194#[derive(Clone, Debug)]
195pub struct SwapProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
196
197impl SwapProof {
198 #![allow(clippy::too_many_arguments)]
199 pub fn prove(
200 blinding_r: Fq,
201 blinding_s: Fq,
202 pk: &ProvingKey<Bls12_377>,
203 public: SwapProofPublic,
204 private: SwapProofPrivate,
205 ) -> anyhow::Result<Self> {
206 let circuit = SwapCircuit { public, private };
207 let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
208 circuit, pk, blinding_r, blinding_s,
209 )
210 .map_err(|err| anyhow::anyhow!(err))?;
211 let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
212 Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
213 Ok(Self(proof_bytes))
214 }
215
216 #[tracing::instrument(skip(self, vk))]
227 pub fn verify(
228 &self,
229 vk: &PreparedVerifyingKey<Bls12_377>,
230 public: SwapProofPublic,
231 ) -> anyhow::Result<()> {
232 let proof =
233 Proof::deserialize_compressed_unchecked(&self.0[..]).map_err(|e| anyhow::anyhow!(e))?;
234
235 let mut public_inputs = Vec::new();
236 public_inputs.extend(
237 public
238 .balance_commitment
239 .0
240 .to_field_elements()
241 .context("balance_commitment should be a Bls12-377 field member")?,
242 );
243 public_inputs.extend(
244 public
245 .swap_commitment
246 .0
247 .to_field_elements()
248 .context("swap_commitment should be a Bls12-377 field member")?,
249 );
250 public_inputs.extend(
251 public
252 .fee_commitment
253 .0
254 .to_field_elements()
255 .context("fee_commitment should be a Bls12-377 field member")?,
256 );
257
258 tracing::trace!(?public_inputs);
259 let start = std::time::Instant::now();
260 let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
261 vk,
262 public_inputs.as_slice(),
263 &proof,
264 )
265 .map_err(|err| anyhow::anyhow!(err))?;
266 tracing::debug!(?proof_result, elapsed = ?start.elapsed());
267 proof_result
268 .then_some(())
269 .ok_or_else(|| anyhow::anyhow!("a swap proof did not verify"))
270 }
271}
272
273impl DomainType for SwapProof {
274 type Proto = pb::ZkSwapProof;
275}
276
277impl From<SwapProof> for pb::ZkSwapProof {
278 fn from(proof: SwapProof) -> Self {
279 pb::ZkSwapProof {
280 inner: proof.0.to_vec(),
281 }
282 }
283}
284
285impl TryFrom<pb::ZkSwapProof> for SwapProof {
286 type Error = anyhow::Error;
287
288 fn try_from(proto: pb::ZkSwapProof) -> Result<Self, Self::Error> {
289 Ok(SwapProof(proto.inner[..].try_into()?))
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296 use penumbra_sdk_asset::{Balance, Value};
297 use penumbra_sdk_keys::keys::{Bip44Path, SeedPhrase, SpendKey};
298 use penumbra_sdk_num::Amount;
299 use proptest::prelude::*;
300
301 fn fr_strategy() -> BoxedStrategy<Fr> {
302 any::<[u8; 32]>()
303 .prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
304 .boxed()
305 }
306
307 prop_compose! {
308 fn arb_valid_swap_statement()(fee_blinding in fr_strategy(), address_index in any::<u32>(), value1_amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>()) -> (SwapProofPublic, SwapProofPrivate) {
309 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
310 let sk_trader = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
311 let fvk_trader = sk_trader.full_viewing_key();
312 let ivk_trader = fvk_trader.incoming();
313 let (claim_address, _dtk_d) = ivk_trader.payment_address(address_index.into());
314
315 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
316 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
317 let trading_pair = TradingPair::new(gm.id(), gn.id());
318
319 let delta_1_i = Amount::from(value1_amount);
320 let delta_2_i = Amount::from(0u64);
321 let fee = Fee::default();
322
323 let rseed = Rseed(rseed_randomness);
324 let swap_plaintext = SwapPlaintext {
325 trading_pair,
326 delta_1_i,
327 delta_2_i,
328 claim_fee: fee,
329 claim_address,
330 rseed,
331 };
332 let fee_commitment = swap_plaintext.claim_fee.commit(fee_blinding);
333 let swap_commitment = swap_plaintext.swap_commitment();
334
335 let value_1 = Value {
336 amount: swap_plaintext.delta_1_i,
337 asset_id: swap_plaintext.trading_pair.asset_1(),
338 };
339 let value_2 = Value {
340 amount: swap_plaintext.delta_2_i,
341 asset_id: swap_plaintext.trading_pair.asset_2(),
342 };
343 let value_fee = Value {
344 amount: swap_plaintext.claim_fee.amount(),
345 asset_id: swap_plaintext.claim_fee.asset_id(),
346 };
347 let mut balance = Balance::default();
348 balance -= value_1;
349 balance -= value_2;
350 balance -= value_fee;
351 let balance_commitment = balance.commit(fee_blinding);
352
353 let public = SwapProofPublic { balance_commitment, swap_commitment, fee_commitment };
354 let private = SwapProofPrivate { fee_blinding, swap_plaintext };
355
356 (public, private)
357 }
358 }
359
360 proptest! {
361 #[test]
362 fn swap_proof_happy_path((public, private) in arb_valid_swap_statement()) {
363 assert!(check_satisfaction(&public, &private).is_ok());
364 assert!(check_circuit_satisfaction(public, private).is_ok());
365 }
366 }
367
368 prop_compose! {
369 fn arb_invalid_swap_statement_fee_commitment()(fee_blinding in fr_strategy(), invalid_fee_blinding in fr_strategy(), address_index in any::<u32>(), value1_amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>()) -> (SwapProofPublic, SwapProofPrivate) {
371 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
372 let sk_trader = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
373 let fvk_trader = sk_trader.full_viewing_key();
374 let ivk_trader = fvk_trader.incoming();
375 let (claim_address, _dtk_d) = ivk_trader.payment_address(address_index.into());
376
377 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
378 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
379 let trading_pair = TradingPair::new(gm.id(), gn.id());
380
381 let delta_1_i = Amount::from(value1_amount);
382 let delta_2_i = Amount::from(0u64);
383 let fee = Fee::default();
384
385 let rseed = Rseed(rseed_randomness);
386 let swap_plaintext = SwapPlaintext {
387 trading_pair,
388 delta_1_i,
389 delta_2_i,
390 claim_fee: fee,
391 claim_address,
392 rseed,
393 };
394 let swap_commitment = swap_plaintext.swap_commitment();
395
396 let value_1 = Value {
397 amount: swap_plaintext.delta_1_i,
398 asset_id: swap_plaintext.trading_pair.asset_1(),
399 };
400 let value_2 = Value {
401 amount: swap_plaintext.delta_2_i,
402 asset_id: swap_plaintext.trading_pair.asset_2(),
403 };
404 let value_fee = Value {
405 amount: swap_plaintext.claim_fee.amount(),
406 asset_id: swap_plaintext.claim_fee.asset_id(),
407 };
408 let mut balance = Balance::default();
409 balance -= value_1;
410 balance -= value_2;
411 balance -= value_fee;
412 let balance_commitment = balance.commit(fee_blinding);
413
414 let invalid_fee_commitment = swap_plaintext.claim_fee.commit(invalid_fee_blinding);
415
416 let public = SwapProofPublic { balance_commitment, swap_commitment, fee_commitment: invalid_fee_commitment };
417 let private = SwapProofPrivate { fee_blinding, swap_plaintext };
418
419 (public, private)
420 }
421 }
422
423 proptest! {
424 #[test]
425 fn swap_proof_invalid_fee_commitment((public, private) in arb_invalid_swap_statement_fee_commitment()) {
426 assert!(check_satisfaction(&public, &private).is_err());
427 assert!(check_circuit_satisfaction(public, private).is_err());
428 }
429 }
430}