penumbra_sdk_governance/delegator_vote/
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::*, uint8::UInt8};
7use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef};
8use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
9use ark_snark::SNARK;
10use base64::{engine::general_purpose, Engine as _};
11use decaf377::{r1cs::FqVar, Bls12_377, Fq, Fr};
12use decaf377_rdsa::{SpendAuth, VerificationKey};
13use penumbra_sdk_asset::{
14    balance::{self, commitment::BalanceCommitmentVar, Commitment},
15    Value,
16};
17use penumbra_sdk_keys::keys::{
18    AuthorizationKeyVar, Bip44Path, IncomingViewingKeyVar, NullifierKey, NullifierKeyVar,
19    RandomizedVerificationKey, SeedPhrase, SpendAuthRandomizerVar, SpendKey,
20};
21use penumbra_sdk_proof_params::{DummyWitness, VerifyingKeyExt, GROTH16_PROOF_LENGTH_BYTES};
22use penumbra_sdk_proto::{core::component::governance::v1 as pb, DomainType};
23use penumbra_sdk_sct::{Nullifier, NullifierVar};
24use penumbra_sdk_shielded_pool::{note, Note, Rseed};
25use penumbra_sdk_tct::{
26    self as tct,
27    r1cs::{PositionVar, StateCommitmentVar},
28    Root,
29};
30use std::str::FromStr;
31use tap::Tap;
32
33/// The public input for a [`DelegatorVoteProof`].
34#[derive(Clone, Debug)]
35pub struct DelegatorVoteProofPublic {
36    /// the merkle root of the state commitment tree.
37    pub anchor: tct::Root,
38    /// balance commitment of the note to be spent.
39    pub balance_commitment: balance::Commitment,
40    /// nullifier of the note to be spent.
41    pub nullifier: Nullifier,
42    /// the randomized verification spend key.
43    pub rk: VerificationKey<SpendAuth>,
44    /// the start position of the proposal being voted on.
45    pub start_position: tct::Position,
46}
47
48/// The private input for a [`DelegatorVoteProof`].
49#[derive(Clone, Debug)]
50pub struct DelegatorVoteProofPrivate {
51    /// Inclusion proof for the note commitment.
52    pub state_commitment_proof: tct::Proof,
53    /// The note being spent.
54    pub note: Note,
55    /// The blinding factor used for generating the balance commitment.
56    pub v_blinding: Fr,
57    /// The randomizer used for generating the randomized spend auth key.
58    pub spend_auth_randomizer: Fr,
59    /// The spend authorization key.
60    pub ak: VerificationKey<SpendAuth>,
61    /// The nullifier deriving key.
62    pub nk: NullifierKey,
63}
64
65#[cfg(test)]
66fn check_satisfaction(
67    public: &DelegatorVoteProofPublic,
68    private: &DelegatorVoteProofPrivate,
69) -> Result<()> {
70    use penumbra_sdk_keys::keys::FullViewingKey;
71
72    let note_commitment = private.note.commit();
73    if note_commitment != private.state_commitment_proof.commitment() {
74        anyhow::bail!("note commitment did not match state commitment proof");
75    }
76
77    let nullifier = Nullifier::derive(
78        &private.nk,
79        private.state_commitment_proof.position(),
80        &note_commitment,
81    );
82    if nullifier != public.nullifier {
83        anyhow::bail!("nullifier did not match public input");
84    }
85
86    private.state_commitment_proof.verify(public.anchor)?;
87
88    let rk = private.ak.randomize(&private.spend_auth_randomizer);
89    if rk != public.rk {
90        anyhow::bail!("randomized spend auth key did not match public input");
91    }
92
93    let fvk = FullViewingKey::from_components(private.ak, private.nk);
94    let ivk = fvk.incoming();
95    let transmission_key = ivk.diversified_public(&private.note.diversified_generator());
96    if transmission_key != *private.note.transmission_key() {
97        anyhow::bail!("transmission key did not match note");
98    }
99
100    let balance_commitment = private.note.value().commit(private.v_blinding);
101    if balance_commitment != public.balance_commitment {
102        anyhow::bail!("balance commitment did not match public input");
103    }
104
105    if private.note.diversified_generator() == decaf377::Element::default() {
106        anyhow::bail!("diversified generator is identity");
107    }
108    if private.ak.is_identity() {
109        anyhow::bail!("ak is identity");
110    }
111
112    if public.start_position.commitment() != 0 {
113        anyhow::bail!("start position commitment index is not zero");
114    }
115
116    if private.state_commitment_proof.position() >= public.start_position {
117        anyhow::bail!("note did not exist prior to the start of voting");
118    }
119
120    Ok(())
121}
122
123#[cfg(test)]
124fn check_circuit_satisfaction(
125    public: DelegatorVoteProofPublic,
126    private: DelegatorVoteProofPrivate,
127) -> Result<()> {
128    use ark_relations::r1cs::{self, ConstraintSystem};
129
130    let cs = ConstraintSystem::new_ref();
131    let circuit = DelegatorVoteCircuit { public, private };
132    cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
133    circuit
134        .generate_constraints(cs.clone())
135        .expect("can generate constraints from circuit");
136    cs.finalize();
137    if !cs.is_satisfied()? {
138        anyhow::bail!("constraints are not satisfied");
139    }
140    Ok(())
141}
142
143/// Groth16 proof for delegator voting.
144#[derive(Clone, Debug)]
145pub struct DelegatorVoteCircuit {
146    public: DelegatorVoteProofPublic,
147    private: DelegatorVoteProofPrivate,
148}
149
150impl ConstraintSynthesizer<Fq> for DelegatorVoteCircuit {
151    fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
152        // Witnesses
153        // Note: In the allocation of the address on `NoteVar` we check the diversified base is not identity.
154        let note_var = note::NoteVar::new_witness(cs.clone(), || Ok(self.private.note.clone()))?;
155        let claimed_note_commitment = StateCommitmentVar::new_witness(cs.clone(), || {
156            Ok(self.private.state_commitment_proof.commitment())
157        })?;
158
159        let delegator_position_var = tct::r1cs::PositionVar::new_witness(cs.clone(), || {
160            Ok(self.private.state_commitment_proof.position())
161        })?;
162        let delegator_position_bits = delegator_position_var.to_bits_le()?;
163        let merkle_path_var = tct::r1cs::MerkleAuthPathVar::new_witness(cs.clone(), || {
164            Ok(self.private.state_commitment_proof)
165        })?;
166
167        let v_blinding_arr: [u8; 32] = self.private.v_blinding.to_bytes();
168        let v_blinding_vars = UInt8::new_witness_vec(cs.clone(), &v_blinding_arr)?;
169
170        let spend_auth_randomizer_var = SpendAuthRandomizerVar::new_witness(cs.clone(), || {
171            Ok(self.private.spend_auth_randomizer)
172        })?;
173        // Note: in the allocation of `AuthorizationKeyVar` we check it is not identity.
174        let ak_element_var: AuthorizationKeyVar =
175            AuthorizationKeyVar::new_witness(cs.clone(), || Ok(self.private.ak))?;
176        let nk_var = NullifierKeyVar::new_witness(cs.clone(), || Ok(self.private.nk))?;
177
178        // Public inputs
179        let anchor_var = FqVar::new_input(cs.clone(), || Ok(Fq::from(self.public.anchor)))?;
180        let claimed_balance_commitment_var =
181            BalanceCommitmentVar::new_input(cs.clone(), || Ok(self.public.balance_commitment))?;
182        let claimed_nullifier_var =
183            NullifierVar::new_input(cs.clone(), || Ok(self.public.nullifier))?;
184        let rk_var = RandomizedVerificationKey::new_input(cs.clone(), || Ok(self.public.rk))?;
185        let start_position = PositionVar::new_input(cs.clone(), || Ok(self.public.start_position))?;
186
187        // Note commitment integrity.
188        let note_commitment_var = note_var.commit()?;
189        note_commitment_var.enforce_equal(&claimed_note_commitment)?;
190
191        // Nullifier integrity.
192        let nullifier_var =
193            NullifierVar::derive(&nk_var, &delegator_position_var, &claimed_note_commitment)?;
194        nullifier_var.enforce_equal(&claimed_nullifier_var)?;
195
196        // Merkle auth path verification against the provided anchor.
197        merkle_path_var.verify(
198            cs.clone(),
199            &Boolean::TRUE,
200            &delegator_position_bits,
201            anchor_var,
202            claimed_note_commitment.inner(),
203        )?;
204
205        // Check integrity of randomized verification key.
206        let computed_rk_var = ak_element_var.randomize(&spend_auth_randomizer_var)?;
207        computed_rk_var.enforce_equal(&rk_var)?;
208
209        // Check integrity of diversified address.
210        let ivk = IncomingViewingKeyVar::derive(&nk_var, &ak_element_var)?;
211        let computed_transmission_key =
212            ivk.diversified_public(&note_var.diversified_generator())?;
213        computed_transmission_key.enforce_equal(&note_var.transmission_key())?;
214
215        // Check integrity of balance commitment.
216        let balance_commitment = note_var.value().commit(v_blinding_vars)?;
217        balance_commitment.enforce_equal(&claimed_balance_commitment_var)?;
218
219        // Additionally, check that the start position has a zero commitment index, since this is
220        // the only sensible start time for a vote.
221        let zero_constant = FqVar::constant(Fq::from(0u64));
222        let commitment = start_position.commitment()?;
223        commitment.enforce_equal(&zero_constant)?;
224
225        // Additionally, check that the position of the spend proof is before the start
226        // start_height, which ensures that the note being voted with was created before voting
227        // started.
228        //
229        // Also note that `FpVar::enforce_cmp` requires that the field elements have size
230        // (p-1)/2, which is true for positions as they are 64 bits at most.
231        //
232        // This MUST be strict inequality (hence passing false to `should_also_check_equality`)
233        // because you could delegate and vote on the proposal in the same block.
234        delegator_position_var.position.enforce_cmp(
235            &start_position.position,
236            core::cmp::Ordering::Less,
237            false,
238        )?;
239
240        Ok(())
241    }
242}
243
244impl DummyWitness for DelegatorVoteCircuit {
245    fn with_dummy_witness() -> Self {
246        let seed_phrase = SeedPhrase::from_randomness(&[b'f'; 32]);
247        let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
248        let fvk_sender = sk_sender.full_viewing_key();
249        let ivk_sender = fvk_sender.incoming();
250        let (address, _dtk_d) = ivk_sender.payment_address(0u32.into());
251
252        let spend_auth_randomizer = Fr::from(1u64);
253        let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
254        let nk = *sk_sender.nullifier_key();
255        let ak = sk_sender.spend_auth_key().into();
256        let note = Note::from_parts(
257            address,
258            Value::from_str("1upenumbra").expect("valid value"),
259            Rseed([1u8; 32]),
260        )
261        .expect("can make a note");
262        let v_blinding = Fr::from(1u64);
263        let rk: VerificationKey<SpendAuth> = rsk.into();
264        let nullifier = Nullifier(Fq::from(1u64));
265        let mut sct = tct::Tree::new();
266        let note_commitment = note.commit();
267        sct.insert(tct::Witness::Keep, note_commitment)
268            .expect("able to insert note commitment into SCT");
269        let anchor = sct.root();
270        let state_commitment_proof = sct
271            .witness(note_commitment)
272            .expect("able to witness just-inserted note commitment");
273        let start_position = state_commitment_proof.position();
274
275        let public = DelegatorVoteProofPublic {
276            anchor,
277            balance_commitment: balance::Commitment(decaf377::Element::GENERATOR),
278            nullifier,
279            rk,
280            start_position,
281        };
282        let private = DelegatorVoteProofPrivate {
283            state_commitment_proof,
284            note,
285            v_blinding,
286            spend_auth_randomizer,
287            ak,
288            nk,
289        };
290
291        Self { public, private }
292    }
293}
294
295#[derive(Debug, thiserror::Error)]
296pub enum VerificationError {
297    #[error("error deserializing compressed proof: {0:?}")]
298    ProofDeserialize(ark_serialize::SerializationError),
299    #[error("Fq types are Bls12-377 field members")]
300    Anchor,
301    #[error("balance commitment is a Bls12-377 field member")]
302    BalanceCommitment,
303    #[error("nullifier is a Bls12-377 field member")]
304    Nullifier,
305    #[error("could not decompress element points: {0:?}")]
306    DecompressRk(decaf377::EncodingError),
307    #[error("randomized spend key is a Bls12-377 field member")]
308    Rk,
309    #[error("start position is a Bls12-377 field member")]
310    StartPosition,
311    #[error("error verifying proof: {0:?}")]
312    SynthesisError(ark_relations::r1cs::SynthesisError),
313    #[error("delegator vote proof did not verify")]
314    InvalidProof,
315}
316
317#[derive(Clone, Debug, Copy)]
318pub struct DelegatorVoteProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
319
320impl DelegatorVoteProof {
321    pub fn prove(
322        blinding_r: Fq,
323        blinding_s: Fq,
324        pk: &ProvingKey<Bls12_377>,
325        public: DelegatorVoteProofPublic,
326        private: DelegatorVoteProofPrivate,
327    ) -> anyhow::Result<Self> {
328        let circuit = DelegatorVoteCircuit { public, private };
329        let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
330            circuit, pk, blinding_r, blinding_s,
331        )
332        .map_err(|err| anyhow::anyhow!(err))?;
333        let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
334        Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
335        Ok(Self(proof_bytes))
336    }
337
338    /// Called to verify the proof using the provided public inputs.
339    // For debugging proof verification failures,
340    // to check that the proof data and verification keys are consistent.
341    #[tracing::instrument(
342        level="debug",
343        skip(self, vk),
344        fields(
345            self = ?general_purpose::STANDARD.encode(self.clone().encode_to_vec()),
346            vk = ?vk.debug_id()
347        )
348    )]
349    pub fn verify(
350        &self,
351        vk: &PreparedVerifyingKey<Bls12_377>,
352        DelegatorVoteProofPublic {
353            anchor: Root(anchor),
354            balance_commitment: Commitment(balance_commitment),
355            nullifier: Nullifier(nullifier),
356            rk,
357            start_position,
358        }: DelegatorVoteProofPublic,
359    ) -> Result<(), VerificationError> {
360        let proof = Proof::deserialize_compressed_unchecked(&self.0[..])
361            .map_err(VerificationError::ProofDeserialize)?;
362        let element_rk = decaf377::Encoding(rk.to_bytes())
363            .vartime_decompress()
364            .map_err(VerificationError::DecompressRk)?;
365
366        /// Shorthand helper, convert expressions into field elements.
367        macro_rules! to_field_elements {
368            ($fe:expr, $err:expr) => {
369                $fe.to_field_elements().ok_or($err)?
370            };
371        }
372
373        use VerificationError::*;
374        let public_inputs = [
375            to_field_elements!(Fq::from(anchor), Anchor),
376            to_field_elements!(balance_commitment, BalanceCommitment),
377            to_field_elements!(nullifier, Nullifier),
378            to_field_elements!(element_rk, Rk),
379            to_field_elements!(start_position, StartPosition),
380        ]
381        .into_iter()
382        .flatten()
383        .collect::<Vec<_>>()
384        .tap(|public_inputs| tracing::trace!(?public_inputs));
385
386        let start = std::time::Instant::now();
387        Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
388            vk,
389            public_inputs.as_slice(),
390            &proof,
391        )
392        .map_err(VerificationError::SynthesisError)?
393        .tap(|proof_result| tracing::debug!(?proof_result, elapsed = ?start.elapsed()))
394        .then_some(())
395        .ok_or(VerificationError::InvalidProof)
396    }
397
398    pub fn to_vec(&self) -> Vec<u8> {
399        self.0.to_vec()
400    }
401}
402
403impl DomainType for DelegatorVoteProof {
404    type Proto = pb::ZkDelegatorVoteProof;
405}
406
407impl From<DelegatorVoteProof> for pb::ZkDelegatorVoteProof {
408    fn from(proof: DelegatorVoteProof) -> Self {
409        pb::ZkDelegatorVoteProof {
410            inner: proof.0.to_vec(),
411        }
412    }
413}
414
415impl TryFrom<pb::ZkDelegatorVoteProof> for DelegatorVoteProof {
416    type Error = anyhow::Error;
417
418    fn try_from(proto: pb::ZkDelegatorVoteProof) -> Result<Self, Self::Error> {
419        Ok(DelegatorVoteProof(proto.inner[..].try_into()?))
420    }
421}
422
423impl TryFrom<&[u8]> for DelegatorVoteProof {
424    type Error = anyhow::Error;
425
426    fn try_from(value: &[u8]) -> Result<Self> {
427        Ok(Self(value.try_into()?))
428    }
429}
430
431#[cfg(test)]
432mod tests {
433
434    use super::*;
435    use decaf377::{Fq, Fr};
436    use penumbra_sdk_asset::{asset, Value};
437    use penumbra_sdk_keys::keys::{SeedPhrase, SpendKey};
438    use penumbra_sdk_num::Amount;
439    use penumbra_sdk_sct::Nullifier;
440    use proptest::prelude::*;
441
442    fn fr_strategy() -> BoxedStrategy<Fr> {
443        any::<[u8; 32]>()
444            .prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
445            .boxed()
446    }
447
448    prop_compose! {
449        fn arb_valid_delegator_vote_statement()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100) -> (DelegatorVoteProofPublic, DelegatorVoteProofPrivate) {
450            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
451            let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
452            let fvk_sender = sk_sender.full_viewing_key();
453            let ivk_sender = fvk_sender.incoming();
454            let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
455            let value_to_send = Value {
456                amount: Amount::from(amount),
457                asset_id: asset::Id(Fq::from(asset_id64)),
458            };
459            let note = Note::from_parts(
460                sender.clone(),
461                value_to_send,
462                Rseed(rseed_randomness),
463            ).expect("should be able to create note");
464            let note_commitment = note.commit();
465            let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
466            let nk = *sk_sender.nullifier_key();
467            let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
468
469            let mut sct = tct::Tree::new();
470
471            // Next, we simulate the case where the SCT is not empty by adding `num_commitments`
472            // unrelated items in the SCT.
473            for i in 0..num_commitments {
474                // To avoid duplicate note commitments, we use the `i` counter as the Rseed randomness
475                let rseed = Rseed([i as u8; 32]);
476                let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
477                sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("can insert note commitment into SCT");
478            }
479
480            sct.insert(tct::Witness::Keep, note_commitment).expect("can insert note commitment into SCT");
481            let anchor = sct.root();
482            let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
483
484            // All proposals should have a position commitment index of zero, so we need to end the epoch
485            // and get the position that corresponds to the first commitment in the new epoch.
486            sct.end_epoch().expect("should be able to end an epoch");
487            let first_note_commitment = Note::from_parts(sender.clone(), value_to_send, Rseed([u8::MAX; 32])).expect("can create note").commit();
488            sct.insert(tct::Witness::Keep, first_note_commitment).expect("can insert note commitment into SCT");
489            let start_position = sct.witness(first_note_commitment).expect("can witness note commitment").position();
490
491            let balance_commitment = value_to_send.commit(v_blinding);
492            let rk: VerificationKey<SpendAuth> = rsk.into();
493            let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), &note_commitment);
494
495            let public = DelegatorVoteProofPublic {
496                anchor,
497                balance_commitment,
498                nullifier,
499                rk,
500                start_position,
501            };
502            let private = DelegatorVoteProofPrivate {
503                state_commitment_proof,
504                note,
505                v_blinding,
506                spend_auth_randomizer,
507                ak,
508                nk,
509            };
510            (public, private)
511        }
512    }
513
514    proptest! {
515        #[test]
516        fn delegator_vote_happy_path((public, private) in arb_valid_delegator_vote_statement()) {
517            assert!(check_satisfaction(&public, &private).is_ok());
518            assert!(check_circuit_satisfaction(public, private).is_ok());
519        }
520    }
521
522    prop_compose! {
523        // This strategy generates a delegator vote statement that votes on a proposal with
524        // a non-zero position commitment index. The circuit should be unsatisfiable in this case.
525        fn arb_invalid_delegator_vote_statement_nonzero_start()(v_blinding in fr_strategy(), spend_auth_randomizer in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), amount in any::<u64>(), seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), num_commitments in 0..100) -> (DelegatorVoteProofPublic, DelegatorVoteProofPrivate) {
526            let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
527            let sk_sender = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
528            let fvk_sender = sk_sender.full_viewing_key();
529            let ivk_sender = fvk_sender.incoming();
530            let (sender, _dtk_d) = ivk_sender.payment_address(address_index.into());
531            let value_to_send = Value {
532                amount: Amount::from(amount),
533                asset_id: asset::Id(Fq::from(asset_id64)),
534            };
535            let note = Note::from_parts(
536                sender.clone(),
537                value_to_send,
538                Rseed(rseed_randomness),
539            ).expect("should be able to create note");
540            let note_commitment = note.commit();
541            let rsk = sk_sender.spend_auth_key().randomize(&spend_auth_randomizer);
542            let nk = *sk_sender.nullifier_key();
543            let ak: VerificationKey<SpendAuth> = sk_sender.spend_auth_key().into();
544
545            let mut sct = tct::Tree::new();
546
547            // Next, we simulate the case where the SCT is not empty by adding `num_commitments`
548            // unrelated items in the SCT.
549            for i in 0..num_commitments {
550                // To avoid duplicate note commitments, we use the `i` counter as the Rseed randomness
551                let rseed = Rseed([i as u8; 32]);
552                let dummy_note_commitment = Note::from_parts(sender.clone(), value_to_send, rseed).expect("can create note").commit();
553                sct.insert(tct::Witness::Keep, dummy_note_commitment).expect("can insert note commitment into SCT");
554            }
555
556            sct.insert(tct::Witness::Keep, note_commitment).expect("can insert note commitment into SCT");
557            let anchor = sct.root();
558            let state_commitment_proof = sct.witness(note_commitment).expect("can witness note commitment");
559
560            let rseed = Rseed([num_commitments as u8; 32]);
561            let not_first_note_commitment = Note::from_parts(sender, value_to_send, rseed).expect("can create note").commit();
562            sct.insert(tct::Witness::Keep, not_first_note_commitment).expect("can insert note commitment into SCT");
563            // All proposals should have a position commitment index of zero, but this one will not, so
564            // expect the proof to fail.
565            let start_position = sct.witness(not_first_note_commitment).expect("can witness note commitment").position();
566
567            let balance_commitment = value_to_send.commit(v_blinding);
568            let rk: VerificationKey<SpendAuth> = rsk.into();
569            let nullifier = Nullifier::derive(&nk, state_commitment_proof.position(), &note_commitment);
570
571            let public = DelegatorVoteProofPublic {
572                anchor,
573                balance_commitment,
574                nullifier,
575                rk,
576                start_position,
577            };
578            let private = DelegatorVoteProofPrivate {
579                state_commitment_proof,
580                note,
581                v_blinding,
582                spend_auth_randomizer,
583                ak,
584                nk,
585            };
586            (public, private)
587        }
588    }
589
590    proptest! {
591        #[test]
592        fn delegator_vote_invalid_start_position((public, private) in arb_invalid_delegator_vote_statement_nonzero_start()) {
593            assert!(check_satisfaction(&public, &private).is_err());
594            assert!(check_circuit_satisfaction(public, private).is_err());
595        }
596    }
597}