1use anyhow::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::{r1cs::FqVar, Bls12_377, Fq};
11use decaf377_rdsa::{SpendAuth, VerificationKey};
12use penumbra_sdk_fee::Fee;
13use penumbra_sdk_proto::{core::component::dex::v1 as pb, DomainType};
14use penumbra_sdk_tct as tct;
15use penumbra_sdk_tct::r1cs::StateCommitmentVar;
16
17use penumbra_sdk_asset::{
18 asset::{self, Id},
19 Value, ValueVar,
20};
21use penumbra_sdk_keys::keys::{
22 AuthorizationKeyVar, Bip44Path, IncomingViewingKeyVar, NullifierKey, NullifierKeyVar,
23 SeedPhrase, SpendKey,
24};
25use penumbra_sdk_num::{Amount, AmountVar};
26use penumbra_sdk_sct::{Nullifier, NullifierVar};
27use penumbra_sdk_shielded_pool::{
28 note::{self, NoteVar},
29 Rseed,
30};
31use tap::Tap;
32use tct::{Root, StateCommitment};
33
34use crate::{
35 batch_swap_output_data::BatchSwapOutputDataVar,
36 swap::{SwapPlaintext, SwapPlaintextVar},
37 BatchSwapOutputData, TradingPair,
38};
39
40use penumbra_sdk_proof_params::{DummyWitness, GROTH16_PROOF_LENGTH_BYTES};
41
42#[derive(Clone, Debug)]
44pub struct SwapClaimProofPublic {
45 pub anchor: tct::Root,
47 pub nullifier: Nullifier,
49 pub claim_fee: Fee,
51 pub output_data: BatchSwapOutputData,
53 pub note_commitment_1: note::StateCommitment,
55 pub note_commitment_2: note::StateCommitment,
57}
58
59#[derive(Clone, Debug)]
61pub struct SwapClaimProofPrivate {
62 pub swap_plaintext: SwapPlaintext,
64 pub state_commitment_proof: tct::Proof,
66 pub ak: VerificationKey<SpendAuth>,
68 pub nk: NullifierKey,
70 pub lambda_1: Amount,
72 pub lambda_2: Amount,
74 pub note_blinding_1: Fq,
76 pub note_blinding_2: Fq,
78}
79
80#[cfg(test)]
81fn check_satisfaction(
82 public: &SwapClaimProofPublic,
83 private: &SwapClaimProofPrivate,
84) -> Result<()> {
85 use penumbra_sdk_keys::FullViewingKey;
86
87 let swap_commitment = private.swap_plaintext.swap_commitment();
88 if swap_commitment != private.state_commitment_proof.commitment() {
89 anyhow::bail!("swap commitment integrity check failed");
90 }
91
92 private.state_commitment_proof.verify(public.anchor)?;
93
94 let nullifier = Nullifier::derive(
95 &private.nk,
96 private.state_commitment_proof.position(),
97 &swap_commitment,
98 );
99 if nullifier != public.nullifier {
100 anyhow::bail!("nullifier did not match public input");
101 }
102
103 let fvk = FullViewingKey::from_components(private.ak, private.nk);
104 let ivk = fvk.incoming();
105 let transmission_key = ivk.diversified_public(private.swap_plaintext.diversified_generator());
106 anyhow::ensure!(
107 transmission_key == *private.swap_plaintext.transmission_key(),
108 "transmission key did not match swap plaintext"
109 );
110 anyhow::ensure!(
111 !private.swap_plaintext.diversified_generator().is_identity(),
112 "diversified generator is identity"
113 );
114 anyhow::ensure!(
115 !private.ak.is_identity(),
116 "diversified generator is identity"
117 );
118
119 if private.swap_plaintext.claim_fee != public.claim_fee {
120 anyhow::bail!("claim fee did not match public input");
121 }
122
123 anyhow::ensure!(
124 private.state_commitment_proof.position().block()
125 == public.output_data.sct_position_prefix.block(),
126 "scm block did not match batch swap"
127 );
128 anyhow::ensure!(
129 private.state_commitment_proof.position().epoch()
130 == public.output_data.sct_position_prefix.epoch(),
131 "scm epoch did not match batch swap"
132 );
133
134 if private.swap_plaintext.trading_pair != public.output_data.trading_pair {
135 anyhow::bail!("trading pair did not match public input");
136 }
137
138 let (lambda_1, lambda_2) = public.output_data.pro_rata_outputs((
139 private.swap_plaintext.delta_1_i,
140 private.swap_plaintext.delta_2_i,
141 ));
142 if lambda_1 != private.lambda_1 {
143 anyhow::bail!("lambda_1 did not match public input");
144 }
145 if lambda_2 != private.lambda_2 {
146 anyhow::bail!("lambda_2 did not match public input");
147 }
148
149 let (output_1_note, output_2_note) = private.swap_plaintext.output_notes(&public.output_data);
150 let note_commitment_1 = output_1_note.commit();
151 let note_commitment_2 = output_2_note.commit();
152 if note_commitment_1 != public.note_commitment_1 {
153 anyhow::bail!("note commitment 1 did not match public input");
154 }
155 if note_commitment_2 != public.note_commitment_2 {
156 anyhow::bail!("note commitment 2 did not match public input");
157 }
158
159 Ok(())
160}
161
162#[cfg(test)]
163fn check_circuit_satisfaction(
164 public: SwapClaimProofPublic,
165 private: SwapClaimProofPrivate,
166) -> Result<()> {
167 use ark_relations::r1cs::{self, ConstraintSystem};
168
169 let cs: ConstraintSystemRef<_> = ConstraintSystem::new_ref();
170 let circuit = SwapClaimCircuit { public, private };
171 cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
172 circuit
173 .generate_constraints(cs.clone())
174 .expect("can generate constraints from circuit");
175 cs.finalize();
176 if !cs.is_satisfied()? {
177 anyhow::bail!("constraints are not satisfied");
178 }
179 Ok(())
180}
181
182#[derive(Clone, Debug)]
184pub struct SwapClaimCircuit {
185 public: SwapClaimProofPublic,
186 private: SwapClaimProofPrivate,
187}
188
189impl ConstraintSynthesizer<Fq> for SwapClaimCircuit {
190 fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
191 let swap_plaintext_var =
195 SwapPlaintextVar::new_witness(cs.clone(), || Ok(self.private.swap_plaintext.clone()))?;
196
197 let claimed_swap_commitment = StateCommitmentVar::new_witness(cs.clone(), || {
198 Ok(self.private.state_commitment_proof.commitment())
199 })?;
200
201 let position_var = tct::r1cs::PositionVar::new_witness(cs.clone(), || {
202 Ok(self.private.state_commitment_proof.position())
203 })?;
204 let position_bits = position_var.to_bits_le()?;
205 let merkle_path_var = tct::r1cs::MerkleAuthPathVar::new_witness(cs.clone(), || {
206 Ok(self.private.state_commitment_proof)
207 })?;
208 let ak_var = AuthorizationKeyVar::new_witness(cs.clone(), || Ok(self.private.ak))?;
210 let nk_var = NullifierKeyVar::new_witness(cs.clone(), || Ok(self.private.nk))?;
211 let lambda_1_i_var = AmountVar::new_witness(cs.clone(), || Ok(self.private.lambda_1))?;
212 let lambda_2_i_var = AmountVar::new_witness(cs.clone(), || Ok(self.private.lambda_2))?;
213 let note_blinding_1 = FqVar::new_witness(cs.clone(), || Ok(self.private.note_blinding_1))?;
214 let note_blinding_2 = FqVar::new_witness(cs.clone(), || Ok(self.private.note_blinding_2))?;
215
216 let anchor_var = FqVar::new_input(cs.clone(), || Ok(Fq::from(self.public.anchor)))?;
218 let claimed_nullifier_var =
219 NullifierVar::new_input(cs.clone(), || Ok(self.public.nullifier))?;
220 let claimed_fee_var = ValueVar::new_input(cs.clone(), || Ok(self.public.claim_fee.0))?;
221 let output_data_var =
222 BatchSwapOutputDataVar::new_input(cs.clone(), || Ok(self.public.output_data))?;
223 let claimed_note_commitment_1 =
224 StateCommitmentVar::new_input(cs.clone(), || Ok(self.public.note_commitment_1))?;
225 let claimed_note_commitment_2 =
226 StateCommitmentVar::new_input(cs.clone(), || Ok(self.public.note_commitment_2))?;
227
228 let swap_commitment = swap_plaintext_var.commit()?;
230 claimed_swap_commitment.enforce_equal(&swap_commitment)?;
231
232 merkle_path_var.verify(
234 cs.clone(),
235 &Boolean::TRUE,
236 &position_bits,
237 anchor_var,
238 claimed_swap_commitment.inner(),
239 )?;
240
241 let nullifier_var = NullifierVar::derive(&nk_var, &position_var, &claimed_swap_commitment)?;
243 nullifier_var.enforce_equal(&claimed_nullifier_var)?;
244
245 let ivk = IncomingViewingKeyVar::derive(&nk_var, &ak_var)?;
247 let computed_transmission_key =
248 ivk.diversified_public(&swap_plaintext_var.claim_address.diversified_generator)?;
249 computed_transmission_key
250 .enforce_equal(&swap_plaintext_var.claim_address.transmission_key)?;
251
252 claimed_fee_var.enforce_equal(&swap_plaintext_var.claim_fee)?;
254
255 output_data_var
257 .block_within_epoch
258 .enforce_equal(&position_var.block()?)?;
259 output_data_var
260 .epoch
261 .enforce_equal(&position_var.epoch()?)?;
262
263 output_data_var
265 .trading_pair
266 .enforce_equal(&swap_plaintext_var.trading_pair)?;
267
268 let (computed_lambda_1_i, computed_lambda_2_i) = output_data_var.pro_rata_outputs(
270 swap_plaintext_var.delta_1_i,
271 swap_plaintext_var.delta_2_i,
272 cs,
273 )?;
274 computed_lambda_1_i.enforce_equal(&lambda_1_i_var)?;
275 computed_lambda_2_i.enforce_equal(&lambda_2_i_var)?;
276
277 let output_1_note = NoteVar {
279 address: swap_plaintext_var.claim_address.clone(),
280 value: ValueVar {
281 amount: lambda_1_i_var,
282 asset_id: swap_plaintext_var.trading_pair.asset_1,
283 },
284 note_blinding: note_blinding_1,
285 };
286 let output_1_commitment = output_1_note.commit()?;
287 let output_2_note = NoteVar {
288 address: swap_plaintext_var.claim_address,
289 value: ValueVar {
290 amount: lambda_2_i_var,
291 asset_id: swap_plaintext_var.trading_pair.asset_2,
292 },
293 note_blinding: note_blinding_2,
294 };
295 let output_2_commitment = output_2_note.commit()?;
296
297 claimed_note_commitment_1.enforce_equal(&output_1_commitment)?;
298 claimed_note_commitment_2.enforce_equal(&output_2_commitment)?;
299
300 Ok(())
301 }
302}
303
304impl DummyWitness for SwapClaimCircuit {
305 fn with_dummy_witness() -> Self {
306 let trading_pair = TradingPair {
307 asset_1: asset::Cache::with_known_assets()
308 .get_unit("upenumbra")
309 .expect("upenumbra denom is known")
310 .id(),
311 asset_2: asset::Cache::with_known_assets()
312 .get_unit("nala")
313 .expect("nala denom is known")
314 .id(),
315 };
316
317 let seed_phrase = SeedPhrase::from_randomness(&[b'f'; 32]);
318 let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
319 let fvk_sender = sk_sender.full_viewing_key();
320 let ivk_sender = fvk_sender.incoming();
321 let (address, _dtk_d) = ivk_sender.payment_address(0u32.into());
322 let ak = *fvk_sender.spend_verification_key();
323 let nk = *sk_sender.nullifier_key();
324
325 let delta_1_i = 10u64.into();
326 let delta_2_i = 1u64.into();
327 let swap_plaintext = SwapPlaintext {
328 trading_pair,
329 delta_1_i,
330 delta_2_i,
331 claim_fee: Fee(Value {
332 amount: 3u64.into(),
333 asset_id: asset::Cache::with_known_assets()
334 .get_unit("upenumbra")
335 .expect("upenumbra denom is known")
336 .id(),
337 }),
338 claim_address: address,
339 rseed: Rseed([1u8; 32]),
340 };
341 let mut sct = tct::Tree::new();
342 let swap_commitment = swap_plaintext.swap_commitment();
343 sct.insert(tct::Witness::Keep, swap_commitment)
344 .expect("insertion of the swap commitment into the SCT should succeed");
345 let anchor = sct.root();
346 let state_commitment_proof = sct
347 .witness(swap_commitment)
348 .expect("the SCT should be able to witness the just-inserted swap commitment");
349 let nullifier = Nullifier(Fq::from(1u64));
350 let claim_fee = Fee::default();
351 let output_data = BatchSwapOutputData {
352 delta_1: Amount::from(10u64),
353 delta_2: Amount::from(10u64),
354 lambda_1: Amount::from(10u64),
355 lambda_2: Amount::from(10u64),
356 unfilled_1: Amount::from(10u64),
357 unfilled_2: Amount::from(10u64),
358 height: 0,
359 trading_pair: swap_plaintext.trading_pair,
360 sct_position_prefix: Default::default(),
361 };
362 let note_blinding_1 = Fq::from(1u64);
363 let note_blinding_2 = Fq::from(1u64);
364 let note_commitment_1 = tct::StateCommitment(Fq::from(1u64));
365 let note_commitment_2 = tct::StateCommitment(Fq::from(2u64));
366 let (lambda_1, lambda_2) = output_data.pro_rata_outputs((delta_1_i, delta_2_i));
367
368 let public = SwapClaimProofPublic {
369 anchor,
370 nullifier,
371 claim_fee,
372 output_data,
373 note_commitment_1,
374 note_commitment_2,
375 };
376 let private = SwapClaimProofPrivate {
377 swap_plaintext,
378 state_commitment_proof,
379 nk,
380 ak,
381 lambda_1,
382 lambda_2,
383 note_blinding_1,
384 note_blinding_2,
385 };
386
387 Self { public, private }
388 }
389}
390
391#[derive(Clone, Debug)]
392pub struct SwapClaimProof(pub [u8; GROTH16_PROOF_LENGTH_BYTES]);
393
394#[derive(Debug, thiserror::Error)]
395pub enum VerificationError {
396 #[error("error deserializing compressed proof: {0:?}")]
397 ProofDeserialize(ark_serialize::SerializationError),
398 #[error("Fq types are Bls12-377 field members")]
399 Anchor,
400 #[error("nullifier is a Bls12-377 field member")]
401 Nullifier,
402 #[error("Fq types are Bls12-377 field members")]
403 ClaimFeeAmount,
404 #[error("asset_id is a Bls12-377 field member")]
405 ClaimFeeAssetId,
406 #[error("output_data is a Bls12-377 field member")]
407 OutputData,
408 #[error("note_commitment_1 is a Bls12-377 field member")]
409 NoteCommitment1,
410 #[error("note_commitment_2 is a Bls12-377 field member")]
411 NoteCommitment2,
412 #[error("error verifying proof: {0:?}")]
413 SynthesisError(ark_relations::r1cs::SynthesisError),
414 #[error("proof did not verify")]
415 InvalidProof,
416}
417
418impl SwapClaimProof {
419 #![allow(clippy::too_many_arguments)]
420 pub fn prove(
423 blinding_r: Fq,
424 blinding_s: Fq,
425 pk: &ProvingKey<Bls12_377>,
426 public: SwapClaimProofPublic,
427 private: SwapClaimProofPrivate,
428 ) -> anyhow::Result<Self> {
429 let circuit = SwapClaimCircuit { public, private };
430
431 let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
432 circuit, pk, blinding_r, blinding_s,
433 )
434 .map_err(|err| anyhow::anyhow!(err))?;
435
436 let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
437 Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
438 Ok(Self(proof_bytes))
439 }
440
441 #[tracing::instrument(skip(self, vk))]
444 pub fn verify(
445 &self,
446 vk: &PreparedVerifyingKey<Bls12_377>,
447 public: SwapClaimProofPublic,
448 ) -> Result<(), VerificationError> {
449 let proof = Proof::deserialize_compressed_unchecked(&self.0[..])
450 .map_err(VerificationError::ProofDeserialize)?;
451
452 let mut public_inputs = Vec::new();
453
454 let SwapClaimProofPublic {
455 anchor: Root(anchor),
456 nullifier: Nullifier(nullifier),
457 claim_fee:
458 Fee(Value {
459 amount,
460 asset_id: Id(asset_id),
461 }),
462 output_data,
463 note_commitment_1: StateCommitment(note_commitment_1),
464 note_commitment_2: StateCommitment(note_commitment_2),
465 } = public;
466
467 public_inputs.extend(
468 Fq::from(anchor)
469 .to_field_elements()
470 .ok_or(VerificationError::Anchor)?,
471 );
472 public_inputs.extend(
473 nullifier
474 .to_field_elements()
475 .ok_or(VerificationError::Nullifier)?,
476 );
477 public_inputs.extend(
478 Fq::from(amount)
479 .to_field_elements()
480 .ok_or(VerificationError::ClaimFeeAmount)?,
481 );
482 public_inputs.extend(
483 asset_id
484 .to_field_elements()
485 .ok_or(VerificationError::ClaimFeeAssetId)?,
486 );
487 public_inputs.extend(
488 output_data
489 .to_field_elements()
490 .ok_or(VerificationError::OutputData)?,
491 );
492 public_inputs.extend(
493 note_commitment_1
494 .to_field_elements()
495 .ok_or(VerificationError::NoteCommitment1)?,
496 );
497 public_inputs.extend(
498 note_commitment_2
499 .to_field_elements()
500 .ok_or(VerificationError::NoteCommitment2)?,
501 );
502
503 tracing::trace!(?public_inputs);
504 let start = std::time::Instant::now();
505 Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
506 vk,
507 public_inputs.as_slice(),
508 &proof,
509 )
510 .map_err(VerificationError::SynthesisError)?
511 .tap(|proof_result| tracing::debug!(?proof_result, elapsed = ?start.elapsed()))
512 .then_some(())
513 .ok_or(VerificationError::InvalidProof)
514 }
515}
516
517impl DomainType for SwapClaimProof {
518 type Proto = pb::ZkSwapClaimProof;
519}
520
521impl From<SwapClaimProof> for pb::ZkSwapClaimProof {
522 fn from(proof: SwapClaimProof) -> Self {
523 pb::ZkSwapClaimProof {
524 inner: proof.0.to_vec(),
525 }
526 }
527}
528
529impl TryFrom<pb::ZkSwapClaimProof> for SwapClaimProof {
530 type Error = anyhow::Error;
531
532 fn try_from(proto: pb::ZkSwapClaimProof) -> Result<Self, Self::Error> {
533 Ok(SwapClaimProof(proto.inner[..].try_into()?))
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use penumbra_sdk_keys::keys::{SeedPhrase, SpendKey};
541 use penumbra_sdk_num::Amount;
542 use proptest::prelude::*;
543
544 #[derive(Debug)]
545 struct TestBatchSwapOutputData {
546 delta_1: Amount,
547 delta_2: Amount,
548 lambda_1: Amount,
549 lambda_2: Amount,
550 unfilled_1: Amount,
551 unfilled_2: Amount,
552 }
553
554 fn filled_bsod_strategy() -> BoxedStrategy<TestBatchSwapOutputData> {
555 let delta_1 = (4001..2000000000u128).prop_map(Amount::from);
556 let delta_2 = (4001..2000000000u128).prop_map(Amount::from);
557
558 let lambda_1 = (2..2000u64).prop_map(Amount::from);
559 let lambda_2 = (2..2000u64).prop_map(Amount::from);
560
561 let unfilled_1 = (2..2000u64).prop_map(Amount::from);
562 let unfilled_2 = (2..2000u64).prop_map(Amount::from);
563
564 (delta_1, delta_2, lambda_1, lambda_2, unfilled_1, unfilled_2)
565 .prop_flat_map(
566 move |(delta_1, delta_2, lambda_1, lambda_2, unfilled_1, unfilled_2)| {
567 (
568 Just(delta_1),
569 Just(delta_2),
570 Just(lambda_1),
571 Just(lambda_2),
572 Just(unfilled_1),
573 Just(unfilled_2),
574 )
575 },
576 )
577 .prop_map(
578 move |(delta_1, delta_2, lambda_1, lambda_2, unfilled_1, unfilled_2)| {
579 TestBatchSwapOutputData {
580 delta_1,
581 delta_2,
582 lambda_1,
583 lambda_2,
584 unfilled_1,
585 unfilled_2,
586 }
587 },
588 )
589 .boxed()
590 }
591
592 fn swapclaim_statement(
593 seed_phrase_randomness: [u8; 32],
594 rseed_randomness: [u8; 32],
595 value1_amount: u64,
596 test_bsod: TestBatchSwapOutputData,
597 ) -> (SwapClaimProofPublic, SwapClaimProofPrivate) {
598 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
599 let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
600 let fvk_recipient = sk_recipient.full_viewing_key();
601 let ivk_recipient = fvk_recipient.incoming();
602 let (claim_address, _dtk_d) = ivk_recipient.payment_address(0u32.into());
603 let nk = *sk_recipient.nullifier_key();
604 let ak = *fvk_recipient.spend_verification_key();
605
606 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
607 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
608 let trading_pair = TradingPair::new(gm.id(), gn.id());
609
610 let delta_1_i = Amount::from(value1_amount);
611 let delta_2_i = Amount::from(0u64);
612 let fee = Fee::default();
613
614 let rseed = Rseed(rseed_randomness);
615 let swap_plaintext = SwapPlaintext {
616 trading_pair,
617 delta_1_i,
618 delta_2_i,
619 claim_fee: fee,
620 claim_address,
621 rseed,
622 };
623 let fee = swap_plaintext.clone().claim_fee;
624 let mut sct = tct::Tree::new();
625 let swap_commitment = swap_plaintext.swap_commitment();
626 sct.insert(tct::Witness::Keep, swap_commitment).unwrap();
627 let anchor = sct.root();
628 let state_commitment_proof = sct.witness(swap_commitment).unwrap();
629 let position = state_commitment_proof.position();
630 let nullifier = Nullifier::derive(&nk, position, &swap_commitment);
631 let epoch_duration = 20;
632 let height = epoch_duration * position.epoch() + position.block();
633
634 let output_data = BatchSwapOutputData {
635 delta_1: test_bsod.delta_1,
636 delta_2: test_bsod.delta_2,
637 lambda_1: test_bsod.lambda_1,
638 lambda_2: test_bsod.lambda_2,
639 unfilled_1: test_bsod.unfilled_1,
640 unfilled_2: test_bsod.unfilled_2,
641 height: height.into(),
642 trading_pair: swap_plaintext.trading_pair,
643 sct_position_prefix: Default::default(),
644 };
645 let (lambda_1, lambda_2) = output_data.pro_rata_outputs((delta_1_i, delta_2_i));
646
647 let (output_rseed_1, output_rseed_2) = swap_plaintext.output_rseeds();
648 let note_blinding_1 = output_rseed_1.derive_note_blinding();
649 let note_blinding_2 = output_rseed_2.derive_note_blinding();
650 let (output_1_note, output_2_note) = swap_plaintext.output_notes(&output_data);
651 let note_commitment_1 = output_1_note.commit();
652 let note_commitment_2 = output_2_note.commit();
653
654 let public = SwapClaimProofPublic {
655 anchor,
656 nullifier,
657 claim_fee: fee,
658 output_data,
659 note_commitment_1,
660 note_commitment_2,
661 };
662 let private = SwapClaimProofPrivate {
663 swap_plaintext,
664 state_commitment_proof,
665 ak,
666 nk,
667 lambda_1,
668 lambda_2,
669 note_blinding_1,
670 note_blinding_2,
671 };
672
673 (public, private)
674 }
675
676 prop_compose! {
677 fn arb_valid_swapclaim_statement_filled()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), value1_amount in 2..200u64, test_bsod in filled_bsod_strategy()) -> (SwapClaimProofPublic, SwapClaimProofPrivate) {
678 swapclaim_statement(seed_phrase_randomness, rseed_randomness, value1_amount, test_bsod)
679 }
680 }
681
682 proptest! {
683 #[test]
684 fn swap_claim_proof_happy_path_filled((public, private) in arb_valid_swapclaim_statement_filled()) {
685 assert!(check_satisfaction(&public, &private).is_ok());
686 assert!(check_circuit_satisfaction(public, private).is_ok());
687 }
688 }
689
690 fn unfilled_bsod_strategy() -> BoxedStrategy<TestBatchSwapOutputData> {
691 let delta_1: Amount = 0u64.into();
692 let delta_2 = (4001..2000000000u128).prop_map(Amount::from);
693
694 let lambda_1: Amount = 0u64.into();
695 let lambda_2: Amount = 0u64.into();
696
697 let unfilled_1: Amount = 0u64.into();
698 let unfilled_2 = delta_2.clone();
699
700 (delta_2, unfilled_2)
701 .prop_flat_map(move |(delta_2, unfilled_2)| (Just(delta_2), Just(unfilled_2)))
702 .prop_map(move |(delta_2, unfilled_2)| TestBatchSwapOutputData {
703 delta_1,
704 delta_2,
705 lambda_1,
706 lambda_2,
707 unfilled_1,
708 unfilled_2,
709 })
710 .boxed()
711 }
712
713 prop_compose! {
714 fn arb_valid_swapclaim_statement_unfilled()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), value1_amount in 2..200u64, test_bsod in unfilled_bsod_strategy()) -> (SwapClaimProofPublic, SwapClaimProofPrivate) {
715 swapclaim_statement(seed_phrase_randomness, rseed_randomness, value1_amount, test_bsod)
716 }
717 }
718
719 proptest! {
720 #[test]
721 fn swap_claim_proof_happy_path_unfilled((public, private) in arb_valid_swapclaim_statement_unfilled()) {
722 assert!(check_satisfaction(&public, &private).is_ok());
723 assert!(check_circuit_satisfaction(public, private).is_ok());
724 }
725 }
726
727 prop_compose! {
728 fn arb_invalid_swapclaim_statement_fee()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), value1_amount in 2..200u64, fee_amount in any::<u64>(), test_bsod in unfilled_bsod_strategy()) -> (SwapClaimProofPublic, SwapClaimProofPrivate) {
730 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
731 let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
732 let fvk_recipient = sk_recipient.full_viewing_key();
733 let ivk_recipient = fvk_recipient.incoming();
734 let (claim_address, _dtk_d) = ivk_recipient.payment_address(0u32.into());
735 let nk = *sk_recipient.nullifier_key();
736 let ak = *fvk_recipient.spend_verification_key();
737
738 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
739 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
740 let trading_pair = TradingPair::new(gm.id(), gn.id());
741
742 let delta_1_i = Amount::from(value1_amount);
743 let delta_2_i = Amount::from(0u64);
744 let fee = Fee::default();
745
746 let rseed = Rseed(rseed_randomness);
747 let swap_plaintext = SwapPlaintext {
748 trading_pair,
749 delta_1_i,
750 delta_2_i,
751 claim_fee: fee,
752 claim_address,
753 rseed,
754 };
755 let incorrect_fee = Fee::from_staking_token_amount(Amount::from(fee_amount));
756 let mut sct = tct::Tree::new();
757 let swap_commitment = swap_plaintext.swap_commitment();
758 sct.insert(tct::Witness::Keep, swap_commitment).unwrap();
759 let anchor = sct.root();
760 let state_commitment_proof = sct.witness(swap_commitment).unwrap();
761 let position = state_commitment_proof.position();
762 let nullifier = Nullifier::derive(&nk, position, &swap_commitment);
763 let epoch_duration = 20;
764 let height = epoch_duration * position.epoch() + position.block();
765
766 let output_data = BatchSwapOutputData {
767 delta_1: test_bsod.delta_1,
768 delta_2: test_bsod.delta_2,
769 lambda_1: test_bsod.lambda_1,
770 lambda_2: test_bsod.lambda_2,
771 unfilled_1: test_bsod.unfilled_1,
772 unfilled_2: test_bsod.unfilled_2,
773 height: height.into(),
774 trading_pair: swap_plaintext.trading_pair,
775 sct_position_prefix: Default::default()
776 };
777 let (lambda_1, lambda_2) = output_data.pro_rata_outputs((delta_1_i, delta_2_i));
778
779 let (output_rseed_1, output_rseed_2) = swap_plaintext.output_rseeds();
780 let note_blinding_1 = output_rseed_1.derive_note_blinding();
781 let note_blinding_2 = output_rseed_2.derive_note_blinding();
782 let (output_1_note, output_2_note) = swap_plaintext.output_notes(&output_data);
783 let note_commitment_1 = output_1_note.commit();
784 let note_commitment_2 = output_2_note.commit();
785
786 let public = SwapClaimProofPublic {
787 anchor,
788 nullifier,
789 claim_fee: incorrect_fee,
790 output_data,
791 note_commitment_1,
792 note_commitment_2,
793 };
794 let private = SwapClaimProofPrivate {
795 swap_plaintext,
796 state_commitment_proof,
797 ak,
798 nk,
799 lambda_1,
800 lambda_2,
801 note_blinding_1,
802 note_blinding_2,
803 };
804
805 (public, private)
806 }
807 }
808
809 proptest! {
810 #[test]
811 fn swap_claim_proof_invalid_fee((public, private) in arb_invalid_swapclaim_statement_fee()) {
812 assert!(check_satisfaction(&public, &private).is_err());
813 assert!(check_circuit_satisfaction(public, private).is_err());
814 }
815 }
816
817 prop_compose! {
818 fn arb_invalid_swapclaim_swap_commitment_height()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), value1_amount in 2..200u64, fee_amount in any::<u64>(), test_bsod in unfilled_bsod_strategy()) -> (SwapClaimProofPublic, SwapClaimProofPrivate) {
821 let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
822 let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
823 let fvk_recipient = sk_recipient.full_viewing_key();
824 let ivk_recipient = fvk_recipient.incoming();
825 let (claim_address, _dtk_d) = ivk_recipient.payment_address(0u32.into());
826 let nk = *fvk_recipient.nullifier_key();
827 let ak = *fvk_recipient.spend_verification_key();
828
829 let gm = asset::Cache::with_known_assets().get_unit("gm").unwrap();
830 let gn = asset::Cache::with_known_assets().get_unit("gn").unwrap();
831 let trading_pair = TradingPair::new(gm.id(), gn.id());
832
833 let delta_1_i = Amount::from(value1_amount);
834 let delta_2_i = Amount::from(0u64);
835 let fee = Fee::default();
836
837 let rseed = Rseed(rseed_randomness);
838 let swap_plaintext = SwapPlaintext {
839 trading_pair,
840 delta_1_i,
841 delta_2_i,
842 claim_fee: fee,
843 claim_address,
844 rseed,
845 };
846 let incorrect_fee = Fee::from_staking_token_amount(Amount::from(fee_amount));
847 let mut sct = tct::Tree::new();
848 let swap_commitment = swap_plaintext.swap_commitment();
849 sct.insert(tct::Witness::Keep, swap_commitment).unwrap();
850 let anchor = sct.root();
851 let state_commitment_proof = sct.witness(swap_commitment).unwrap();
852 let position = state_commitment_proof.position();
853 let nullifier = Nullifier::derive(&nk, position, &swap_commitment);
854
855 sct.end_block().expect("can end block");
858 let dummy_swap_commitment = tct::StateCommitment(Fq::from(1u64));
859 sct.insert(tct::Witness::Keep, dummy_swap_commitment).unwrap();
860 let dummy_state_commitment_proof = sct.witness(swap_commitment).unwrap();
861 let dummy_position = dummy_state_commitment_proof.position();
862
863 let epoch_duration = 20;
864 let height = epoch_duration * dummy_position.epoch() + dummy_position.block();
865
866 let output_data = BatchSwapOutputData {
867 delta_1: test_bsod.delta_1,
868 delta_2: test_bsod.delta_2,
869 lambda_1: test_bsod.lambda_1,
870 lambda_2: test_bsod.lambda_2,
871 unfilled_1: test_bsod.unfilled_1,
872 unfilled_2: test_bsod.unfilled_2,
873 height: height.into(),
874 trading_pair: swap_plaintext.trading_pair,
875 sct_position_prefix: Default::default()
876 };
877 let (lambda_1, lambda_2) = output_data.pro_rata_outputs((delta_1_i, delta_2_i));
878
879 let (output_rseed_1, output_rseed_2) = swap_plaintext.output_rseeds();
880 let note_blinding_1 = output_rseed_1.derive_note_blinding();
881 let note_blinding_2 = output_rseed_2.derive_note_blinding();
882 let (output_1_note, output_2_note) = swap_plaintext.output_notes(&output_data);
883 let note_commitment_1 = output_1_note.commit();
884 let note_commitment_2 = output_2_note.commit();
885
886 let public = SwapClaimProofPublic {
887 anchor,
888 nullifier,
889 claim_fee: incorrect_fee,
890 output_data,
891 note_commitment_1,
892 note_commitment_2,
893 };
894 let private = SwapClaimProofPrivate {
895 swap_plaintext,
896 state_commitment_proof,
897 ak,
898 nk,
899 lambda_1,
900 lambda_2,
901 note_blinding_1,
902 note_blinding_2,
903 };
904
905 (public, private)
906 }
907 }
908
909 proptest! {
910 #[test]
911 fn swap_claim_proof_invalid_swap_commitment_height((public, private) in arb_invalid_swapclaim_swap_commitment_height()) {
912 assert!(check_satisfaction(&public, &private).is_err());
913 assert!(check_circuit_satisfaction(public, private).is_err());
914 }
915 }
916}