penumbra_sdk_dex/swap_claim/
proof.rs

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/// The public inputs to a [`SwapProofPublic`].
43#[derive(Clone, Debug)]
44pub struct SwapClaimProofPublic {
45    /// Anchor
46    pub anchor: tct::Root,
47    /// Nullifier
48    pub nullifier: Nullifier,
49    /// Fee
50    pub claim_fee: Fee,
51    /// Batch swap output data
52    pub output_data: BatchSwapOutputData,
53    /// Note commitment of first output note
54    pub note_commitment_1: note::StateCommitment,
55    /// Note commitment of second output note
56    pub note_commitment_2: note::StateCommitment,
57}
58
59/// The public inputs to a [`SwapProofPrivate`].
60#[derive(Clone, Debug)]
61pub struct SwapClaimProofPrivate {
62    /// The swap being claimed
63    pub swap_plaintext: SwapPlaintext,
64    /// Inclusion proof for the swap commitment
65    pub state_commitment_proof: tct::Proof,
66    /// The spend verification key
67    pub ak: VerificationKey<SpendAuth>,
68    // The nullifier deriving key for the swap.
69    pub nk: NullifierKey,
70    /// Output amount 1
71    pub lambda_1: Amount,
72    /// Output amount 2
73    pub lambda_2: Amount,
74    /// Note commitment blinding factor for the first output note
75    pub note_blinding_1: Fq,
76    /// Note commitment blinding factor for the second output note
77    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/// SwapClaim consumes an existing Swap so they are most similar to Spend operations.
183#[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        // Witnesses
192        // Note: in the allocation of the address on the `SwapPlaintextVar`, we check the diversified
193        // base is not identity.
194        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        // Note: in the allocation of `AuthorizationKeyVar` we check it is not identity.
209        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        // Inputs
217        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        // Swap commitment integrity check.
229        let swap_commitment = swap_plaintext_var.commit()?;
230        claimed_swap_commitment.enforce_equal(&swap_commitment)?;
231
232        // Merkle path integrity. Ensure the provided swap commitment is in the TCT.
233        merkle_path_var.verify(
234            cs.clone(),
235            &Boolean::TRUE,
236            &position_bits,
237            anchor_var,
238            claimed_swap_commitment.inner(),
239        )?;
240
241        // Nullifier integrity.
242        let nullifier_var = NullifierVar::derive(&nk_var, &position_var, &claimed_swap_commitment)?;
243        nullifier_var.enforce_equal(&claimed_nullifier_var)?;
244
245        // Connection between nullifier key and address
246        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        // Fee consistency check.
253        claimed_fee_var.enforce_equal(&swap_plaintext_var.claim_fee)?;
254
255        // Validate the swap commitment's height matches the output data's height (i.e. the clearing price height).
256        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        // Validate that the output data's trading pair matches the note commitment's trading pair.
264        output_data_var
265            .trading_pair
266            .enforce_equal(&swap_plaintext_var.trading_pair)?;
267
268        // Output amounts integrity
269        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        // Output note integrity
278        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    /// Generate an [`SwapClaimProof`] given the proving key, public inputs,
421    /// witness data, and two random elements `blinding_r` and `blinding_s`.
422    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    /// Called to verify the proof using the provided public inputs.
442    //#[tracing::instrument(skip(self, vk), fields(self = ?base64::encode(&self.clone().encode_to_vec()), vk = ?vk.debug_id()))]
443    #[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        // This strategy is invalid because the fee is not equal to the claim fee.
729        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        // This strategy is invalid because the block height of the swap commitment does not match
819        // the height of the batch swap output data.
820        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        // End the block, and then add a dummy commitment that we'll use
856        // to compute the position and block height that the BSOD corresponds to.
857        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}