1use std::string::String;
13use std::vec::Vec;
14
15use ff::{Field, FromUniformBytes, PrimeField};
16use group::{Curve, GroupEncoding};
17use halo2_proofs::circuit::Value;
18use pasta_curves::{
19 arithmetic::{Coordinates, CurveAffine},
20 pallas,
21};
22
23use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey};
24
25use super::circuit::{
26 van_integrity_hash, van_nullifier_hash, vote_commitment_hash, Circuit, Instance,
27 MAX_PROPOSAL_ID, VOTE_COMM_TREE_DEPTH,
28};
29use super::prove::create_vote_proof;
30use crate::circuit::elgamal::{base_to_scalar, spend_auth_g_affine};
31use crate::shares_hash::{share_commitment, shares_hash};
32use crate::{domain_tags, ProveError};
33
34const BALLOT_DIVISOR: u64 = 12_500_000;
36
37const NUM_SHARES: usize = 16;
39
40const DENOMINATIONS: [u64; 8] = [10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
53
54type PallasAffineCoordinates = Coordinates<pallas::Affine>;
55
56const MAX_DENOM_SHARES: usize = 9;
64
65const _: () = assert!(
68 NUM_SHARES - MAX_DENOM_SHARES >= 7,
69 "need at least 7 remainder slots for PRF-weighted distribution"
70);
71
72fn denomination_split(
84 num_ballots: u64,
85 sk: &SpendingKey,
86 round_id: pallas::Base,
87 proposal_id: u64,
88 van_commitment: pallas::Base,
89) -> [u64; NUM_SHARES] {
90 let mut shares = [0u64; NUM_SHARES];
91 let mut remaining = num_ballots;
92 let mut idx = 0;
93
94 for &d in &DENOMINATIONS {
98 while remaining >= d && idx < MAX_DENOM_SHARES {
99 shares[idx] = d;
100 remaining -= d;
101 idx += 1;
102 }
103 }
104
105 if remaining > 0 {
110 distribute_remainder(
111 &mut shares[idx..],
112 remaining,
113 sk,
114 round_id,
115 proposal_id,
116 van_commitment,
117 idx as u8,
118 );
119 }
120
121 shares
122}
123
124fn distribute_remainder(
130 slots: &mut [u64],
131 remainder: u64,
132 sk: &SpendingKey,
133 round_id: pallas::Base,
134 proposal_id: u64,
135 van_commitment: pallas::Base,
136 base_index: u8,
137) {
138 let n = slots.len() as u64;
139 if remainder < n {
147 for i in 0..(remainder as usize) {
148 slots[i] = 1;
149 }
150 return;
151 }
152
153 let distributable = remainder - n;
159
160 let mut weights = Vec::with_capacity(slots.len());
164 let mut total_weight: u64 = 0;
165 for i in 0..slots.len() {
166 let hash = vote_share_prf(
167 sk,
168 domain_tags::VOTE_PRF_DOMAIN_REMAINDER,
169 round_id,
170 proposal_id,
171 van_commitment,
172 base_index.wrapping_add(i as u8),
173 );
174 let w = u32::from_le_bytes(hash[0..4].try_into().unwrap()) as u64 | 1;
175 weights.push(w);
176 total_weight += w;
177 }
178
179 let mut assigned: u64 = 0;
183 for i in 0..slots.len() {
184 let share = ((distributable as u128 * weights[i] as u128) / total_weight as u128) as u64;
185 slots[i] = 1 + share;
186 assigned += share;
187 }
188
189 let leftover = distributable - assigned;
193 for i in 0..(leftover as usize) {
194 slots[i] += 1;
195 }
196}
197
198#[derive(Debug, Clone)]
204pub struct EncryptedShareOutput {
205 pub c1: [u8; 32],
207 pub c2: [u8; 32],
209 pub share_index: u32,
211 pub plaintext_value: u64,
213 pub randomness: [u8; 32],
216}
217
218#[derive(Debug)]
220pub struct VoteProofBundle {
221 pub proof: Vec<u8>,
223 pub instance: Instance,
225 pub r_vpk_bytes: [u8; 32],
227 pub encrypted_shares: [EncryptedShareOutput; 16],
231 pub shares_hash: pallas::Base,
237 pub share_blinds: [pallas::Base; 16],
242 pub share_comms: [pallas::Base; 16],
251}
252
253#[derive(Debug)]
255pub enum VoteProofBuildError {
256 InvalidRandomness(String),
258 InvalidShares(String),
260 InvalidElectionPublicKey,
262 InvalidRandomizedVotingPublicKey,
264 InvalidEncryptedShare(String),
266 InvalidProposalId(u64),
268 Prove(ProveError),
270}
271
272impl From<ProveError> for VoteProofBuildError {
273 fn from(error: ProveError) -> Self {
274 VoteProofBuildError::Prove(error)
275 }
276}
277
278impl core::fmt::Display for VoteProofBuildError {
279 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
280 match self {
281 VoteProofBuildError::InvalidRandomness(msg) => {
282 write!(f, "invalid randomness: {}", msg)
283 }
284 VoteProofBuildError::InvalidShares(msg) => {
285 write!(f, "invalid shares: {}", msg)
286 }
287 VoteProofBuildError::InvalidElectionPublicKey => {
288 write!(f, "invalid election public key: identity point")
289 }
290 VoteProofBuildError::InvalidRandomizedVotingPublicKey => {
291 write!(f, "invalid randomized voting public key: identity point")
292 }
293 VoteProofBuildError::InvalidEncryptedShare(msg) => {
294 write!(f, "invalid encrypted share: {}", msg)
295 }
296 VoteProofBuildError::InvalidProposalId(proposal_id) => {
297 write!(
298 f,
299 "proposal_id must be in [1, {}], got {}",
300 MAX_PROPOSAL_ID - 1,
301 proposal_id
302 )
303 }
304 VoteProofBuildError::Prove(error) => {
305 write!(f, "proof generation failed: {error}")
306 }
307 }
308 }
309}
310
311impl std::error::Error for VoteProofBuildError {
312 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
313 match self {
314 VoteProofBuildError::Prove(error) => Some(error),
315 _ => None,
316 }
317 }
318}
319
320fn pallas_coordinates(point: pallas::Affine) -> Option<PallasAffineCoordinates> {
321 point.coordinates().into()
322}
323
324fn encrypted_share_coordinates(
325 point: pallas::Affine,
326 share_index: usize,
327 component: &'static str,
328) -> Result<PallasAffineCoordinates, VoteProofBuildError> {
329 pallas_coordinates(point).ok_or_else(|| {
330 VoteProofBuildError::InvalidEncryptedShare(format!(
331 "share {} {} is identity",
332 share_index, component
333 ))
334 })
335}
336
337fn extract_vsk(sk: &SpendingKey) -> pallas::Scalar {
342 let ask_raw = SpendAuthorizingKey::derive_inner(sk);
343 let g = pallas::Point::from(spend_auth_g_affine());
344 let ak_point = (g * ask_raw).to_affine();
345 let ak_bytes = ak_point.to_bytes();
346
347 if (ak_bytes.as_ref()[31] >> 7) == 1 {
349 -ask_raw
350 } else {
351 ask_raw
352 }
353}
354
355fn vote_share_prf(
366 sk: &SpendingKey,
367 domain: u8,
368 round_id: pallas::Base,
369 proposal_id: u64,
370 van_commitment: pallas::Base,
371 share_index: u8,
372) -> [u8; 64] {
373 *blake2b_simd::Params::new()
374 .hash_length(64)
375 .personal(domain_tags::VOTE_PRF_PERSONALIZATION)
376 .to_state()
377 .update(sk.to_bytes())
378 .update(&[domain])
379 .update(&round_id.to_repr())
380 .update(&proposal_id.to_le_bytes())
381 .update(&van_commitment.to_repr())
382 .update(&[share_index])
383 .finalize()
384 .as_array()
385}
386
387fn derive_share_randomness(
393 sk: &SpendingKey,
394 round_id: pallas::Base,
395 proposal_id: u64,
396 van_commitment: pallas::Base,
397 share_index: u8,
398) -> pallas::Base {
399 let hash = vote_share_prf(
400 sk,
401 domain_tags::VOTE_PRF_DOMAIN_ELGAMAL,
402 round_id,
403 proposal_id,
404 van_commitment,
405 share_index,
406 );
407 let r = pallas::Base::from_uniform_bytes(&hash);
408 if bool::from(r.is_zero()) {
409 return pallas::Base::one();
412 }
413 debug_assert!(base_to_scalar(r).is_some(), "p < q guarantees Base→Scalar");
414 r
415}
416
417fn derive_share_blind(
419 sk: &SpendingKey,
420 round_id: pallas::Base,
421 proposal_id: u64,
422 van_commitment: pallas::Base,
423 share_index: u8,
424) -> pallas::Base {
425 let hash = vote_share_prf(
426 sk,
427 domain_tags::VOTE_PRF_DOMAIN_BLIND,
428 round_id,
429 proposal_id,
430 van_commitment,
431 share_index,
432 );
433 pallas::Base::from_uniform_bytes(&hash)
434}
435
436fn deterministic_shuffle(
450 shares: &mut [u64; NUM_SHARES],
451 sk: &SpendingKey,
452 round_id: pallas::Base,
453 proposal_id: u64,
454 van_commitment: pallas::Base,
455) {
456 let seed = vote_share_prf(
460 sk,
461 domain_tags::VOTE_PRF_DOMAIN_SHUFFLE,
462 round_id,
463 proposal_id,
464 van_commitment,
465 0,
466 );
467 for i in (1..NUM_SHARES).rev() {
468 let byte_offset = (NUM_SHARES - 1 - i) * 4;
472 let rand_bytes: [u8; 4] = seed[byte_offset..byte_offset + 4]
473 .try_into()
474 .expect("64-byte seed has room for 15 × 4-byte draws");
475 let j = (u32::from_le_bytes(rand_bytes) as usize) % (i + 1);
476 shares.swap(i, j);
477 }
478}
479
480#[allow(clippy::too_many_arguments)]
533pub fn build_vote_proof_from_delegation(
534 sk: &SpendingKey,
535 address_index: u32,
536 total_note_value: u64,
537 van_comm_rand: pallas::Base,
538 voting_round_id: pallas::Base,
539 vote_comm_tree_path: [pallas::Base; VOTE_COMM_TREE_DEPTH],
540 vote_comm_tree_position: u32,
541 anchor_height: u32,
542 proposal_id: u64,
543 vote_decision: u64,
544 ea_pk: pallas::Affine,
545 alpha_v: pallas::Scalar,
546 proposal_authority_old_u64: u64,
547 single_share: bool,
548) -> Result<VoteProofBundle, VoteProofBuildError> {
549 if proposal_id == 0 || proposal_id >= MAX_PROPOSAL_ID as u64 {
550 return Err(VoteProofBuildError::InvalidProposalId(proposal_id));
551 }
552
553 let ea_pk_coords =
554 pallas_coordinates(ea_pk).ok_or(VoteProofBuildError::InvalidElectionPublicKey)?;
555 let ea_pk_x = *ea_pk_coords.x();
556 let ea_pk_y = *ea_pk_coords.y();
557 let ea_pk_point = pallas::Point::from(ea_pk);
558
559 let vsk = extract_vsk(sk);
562 let fvk: FullViewingKey = sk.into();
563 let vsk_nk = fvk.nk().inner();
564 let rivk_v = fvk.rivk(Scope::External).inner();
565
566 let address = fvk.address_at(address_index, Scope::External);
567 let vpk_g_d_affine = address.g_d().to_affine();
568 let vpk_pk_d_affine = address.pk_d().inner().to_affine();
569
570 let vpk_g_d_coords = pallas_coordinates(vpk_g_d_affine)
571 .expect("orchard address g_d is non-identity by construction");
572 let vpk_pk_d_coords = pallas_coordinates(vpk_pk_d_affine)
573 .expect("orchard address pk_d is non-identity by construction");
574 let vpk_g_d_x = *vpk_g_d_coords.x();
575 let vpk_pk_d_x = *vpk_pk_d_coords.x();
576
577 {
579 use core::iter;
580 use group::ff::PrimeFieldBits;
581 use halo2_gadgets::sinsemilla::primitives::CommitDomain;
582 use orchard::constants::{fixed_bases::COMMIT_IVK_PERSONALIZATION, L_ORCHARD_BASE};
583
584 let ak_from_vsk = (pallas::Point::from(spend_auth_g_affine()) * vsk).to_affine();
586 let fvk_bytes = fvk.to_bytes();
587 let ak_from_fvk_bytes: [u8; 32] = fvk_bytes[0..32].try_into().unwrap();
588 let ak_from_fvk: pallas::Affine = {
589 let opt: Option<pallas::Point> = pallas::Point::from_bytes(&ak_from_fvk_bytes).into();
590 opt.expect("ak from fvk must be a valid point").to_affine()
591 };
592 assert_eq!(
593 ak_from_vsk, ak_from_fvk,
594 "extract_vsk bug: [vsk]*SpendAuthG != ak from FullViewingKey"
595 );
596
597 let ak_from_vsk_coords = pallas_coordinates(ak_from_vsk)
599 .expect("valid Orchard spending keys have nonzero spend authorizing keys");
600 let ak_x = *ak_from_vsk_coords.x();
601 let domain = CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
602 let ivk = domain
603 .short_commit(
604 iter::empty()
605 .chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
606 .chain(vsk_nk.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
607 &rivk_v,
608 )
609 .expect("CommitIvk must not produce bottom");
610 let ivk_scalar = base_to_scalar(ivk).expect("ivk must be convertible to scalar");
611 let pk_d_derived = (pallas::Point::from(vpk_g_d_affine) * ivk_scalar).to_affine();
612 assert_eq!(
613 pk_d_derived, vpk_pk_d_affine,
614 "CommitIvk chain mismatch: [ivk]*g_d != pk_d from address"
615 );
616 }
617
618 let proposal_authority_old = pallas::Base::from(proposal_authority_old_u64);
621 let one_shifted = pallas::Base::from(1u64 << proposal_id);
622 let proposal_authority_new = proposal_authority_old - one_shifted;
623
624 let num_ballots = total_note_value / BALLOT_DIVISOR;
627 let num_ballots_base = pallas::Base::from(num_ballots);
628
629 let vote_authority_note_old = van_integrity_hash(
634 vpk_g_d_x,
635 vpk_pk_d_x,
636 num_ballots_base,
637 voting_round_id,
638 proposal_authority_old,
639 van_comm_rand,
640 );
641
642 let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
643
644 let vote_authority_note_new = van_integrity_hash(
645 vpk_g_d_x,
646 vpk_pk_d_x,
647 num_ballots_base,
648 voting_round_id,
649 proposal_authority_new,
650 van_comm_rand,
651 );
652
653 let shares_u64: [u64; 16] = if single_share {
658 let mut s = [0u64; 16];
662 s[0] = num_ballots;
663 s
664 } else {
665 let mut s = denomination_split(
666 num_ballots,
667 sk,
668 voting_round_id,
669 proposal_id,
670 vote_authority_note_old,
671 );
672 deterministic_shuffle(
673 &mut s,
674 sk,
675 voting_round_id,
676 proposal_id,
677 vote_authority_note_old,
678 );
679 s
680 };
681
682 for (i, &s) in shares_u64.iter().enumerate() {
684 if s >= (1u64 << 30) {
685 return Err(VoteProofBuildError::InvalidShares(format!(
686 "share {} = {} exceeds 2^30",
687 i, s
688 )));
689 }
690 }
691
692 let shares_base: [pallas::Base; 16] =
693 core::array::from_fn(|i| pallas::Base::from(shares_u64[i]));
694
695 let g = pallas::Point::from(spend_auth_g_affine());
701 let mut enc_c1_x = [pallas::Base::zero(); 16];
702 let mut enc_c2_x = [pallas::Base::zero(); 16];
703 let mut enc_c1_y = [pallas::Base::zero(); 16];
704 let mut enc_c2_y = [pallas::Base::zero(); 16];
705 let mut share_randomness = [pallas::Base::zero(); 16];
706 let mut enc_share_outputs: [EncryptedShareOutput; 16] =
707 core::array::from_fn(|i| EncryptedShareOutput {
708 c1: [0u8; 32],
709 c2: [0u8; 32],
710 share_index: i as u32,
711 plaintext_value: shares_u64[i],
712 randomness: [0u8; 32],
713 });
714
715 for i in 0..16 {
716 let r = derive_share_randomness(
717 sk,
718 voting_round_id,
719 proposal_id,
720 vote_authority_note_old,
721 i as u8,
722 );
723 share_randomness[i] = r;
724 let r_scalar =
725 base_to_scalar(r).expect("derive_share_randomness guarantees nonzero scalar-range");
726 let v_scalar = base_to_scalar(shares_base[i]).expect("share value in range");
727
728 let c1_point = (g * r_scalar).to_affine();
729 let c2_point = (g * v_scalar + ea_pk_point * r_scalar).to_affine();
730
731 let c1_coords = encrypted_share_coordinates(c1_point, i, "c1")?;
732 let c2_coords = encrypted_share_coordinates(c2_point, i, "c2")?;
733
734 enc_c1_x[i] = *c1_coords.x();
735 enc_c2_x[i] = *c2_coords.x();
736 enc_c1_y[i] = *c1_coords.y();
737 enc_c2_y[i] = *c2_coords.y();
738
739 enc_share_outputs[i].c1 = c1_point.to_bytes();
740 enc_share_outputs[i].c2 = c2_point.to_bytes();
741 enc_share_outputs[i].randomness = r.to_repr();
742 }
743
744 let share_blinds: [pallas::Base; 16] = core::array::from_fn(|i| {
745 derive_share_blind(
746 sk,
747 voting_round_id,
748 proposal_id,
749 vote_authority_note_old,
750 i as u8,
751 )
752 });
753 let share_comms: [pallas::Base; 16] = core::array::from_fn(|i| {
754 share_commitment(
755 share_blinds[i],
756 enc_c1_x[i],
757 enc_c2_x[i],
758 enc_c1_y[i],
759 enc_c2_y[i],
760 )
761 });
762 let shares_hash_val = shares_hash(share_blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
763
764 let ak_point = pallas::Point::from(spend_auth_g_affine()) * vsk;
767 let r_vpk = (ak_point + pallas::Point::from(spend_auth_g_affine()) * alpha_v).to_affine();
768 let r_vpk_coords =
769 pallas_coordinates(r_vpk).ok_or(VoteProofBuildError::InvalidRandomizedVotingPublicKey)?;
770 let r_vpk_x = *r_vpk_coords.x();
771 let r_vpk_y = *r_vpk_coords.y();
772 let r_vpk_bytes: [u8; 32] = r_vpk.to_bytes();
773
774 let proposal_id_base = pallas::Base::from(proposal_id);
777 let vote_decision_base = pallas::Base::from(vote_decision);
778 let vote_commitment = vote_commitment_hash(
779 voting_round_id,
780 shares_hash_val,
781 proposal_id_base,
782 vote_decision_base,
783 );
784
785 let vote_comm_tree_root = {
789 use super::circuit::poseidon_hash_2;
790
791 let mut current = vote_authority_note_old;
792 for level in 0..VOTE_COMM_TREE_DEPTH {
793 let sibling = vote_comm_tree_path[level];
794 if vote_comm_tree_position & (1 << level) == 0 {
795 current = poseidon_hash_2(current, sibling);
796 } else {
797 current = poseidon_hash_2(sibling, current);
798 }
799 }
800 current
801 };
802
803 let mut circuit = Circuit::with_van_witnesses(
806 Value::known(vote_comm_tree_path),
807 Value::known(vote_comm_tree_position),
808 Value::known(vpk_g_d_affine),
809 Value::known(vpk_pk_d_affine),
810 Value::known(num_ballots_base),
811 Value::known(proposal_authority_old),
812 Value::known(van_comm_rand),
813 Value::known(vote_authority_note_old),
814 Value::known(vsk),
815 Value::known(rivk_v),
816 Value::known(vsk_nk),
817 Value::known(alpha_v),
818 );
819 circuit.one_shifted = Value::known(one_shifted);
820 circuit.shares = shares_base.map(Value::known);
821 circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
822 circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
823 circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
824 circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
825 circuit.share_blinds = share_blinds.map(Value::known);
826 circuit.share_randomness = share_randomness.map(Value::known);
827 circuit.ea_pk = Value::known(ea_pk);
828 circuit.vote_decision = Value::known(vote_decision_base);
829
830 let anchor_height_base = pallas::Base::from(u64::from(anchor_height));
833 let instance = Instance::from_parts(
834 van_nullifier,
835 r_vpk_x,
836 r_vpk_y,
837 vote_authority_note_new,
838 vote_commitment,
839 vote_comm_tree_root,
840 anchor_height_base,
841 proposal_id_base,
842 voting_round_id,
843 ea_pk_x,
844 ea_pk_y,
845 );
846
847 let proof = create_vote_proof(circuit, &instance)?;
850
851 Ok(VoteProofBundle {
852 proof,
853 instance,
854 r_vpk_bytes,
855 encrypted_shares: enc_share_outputs,
856 shares_hash: shares_hash_val,
857 share_blinds,
858 share_comms,
859 })
860}
861
862#[cfg(test)]
863mod tests {
864 use super::*;
865 use ff::Field;
866 use group::Group;
867
868 fn test_sk() -> SpendingKey {
869 SpendingKey::from_bytes([0x42; 32]).expect("valid spending key")
870 }
871
872 fn test_round_id() -> pallas::Base {
873 pallas::Base::from(0xCAFE_u64)
874 }
875
876 fn test_van() -> pallas::Base {
877 pallas::Base::from(0xDEAD_u64)
878 }
879
880 #[test]
881 fn vote_share_prf_has_frozen_test_vector() {
882 let hash = vote_share_prf(
883 &test_sk(),
884 crate::domain_tags::VOTE_PRF_DOMAIN_ELGAMAL,
885 test_round_id(),
886 1,
887 test_van(),
888 0,
889 );
890
891 assert_eq!(
892 hash,
893 [
894 0x62, 0x03, 0x29, 0x9b, 0x2c, 0x58, 0x4b, 0xa6, 0x37, 0x4d, 0xbe, 0xd6, 0x45, 0x71,
895 0x6f, 0x03, 0x31, 0x56, 0x95, 0x6f, 0xf1, 0x88, 0x8e, 0x75, 0x41, 0x43, 0xb1, 0xf5,
896 0x54, 0xea, 0xb5, 0xb0, 0x6b, 0xdf, 0x7d, 0xca, 0xd4, 0x5a, 0xc2, 0xf4, 0xb9, 0x6a,
897 0xe4, 0x5b, 0xb9, 0x98, 0xd0, 0x5b, 0x4a, 0x8f, 0x12, 0x49, 0x52, 0xb3, 0x0b, 0x19,
898 0xc1, 0xaf, 0x89, 0x35, 0x8a, 0x96, 0xe0, 0x2c,
899 ]
900 );
901 }
902
903 #[test]
904 fn build_vote_proof_rejects_invalid_proposal_id() {
905 let sk = test_sk();
906
907 for proposal_id in [0, MAX_PROPOSAL_ID as u64, 64] {
908 let err = build_vote_proof_from_delegation(
909 &sk,
910 1,
911 BALLOT_DIVISOR,
912 test_van(),
913 test_round_id(),
914 [pallas::Base::from(0u64); VOTE_COMM_TREE_DEPTH],
915 0,
916 123,
917 proposal_id,
918 1,
919 pallas::Point::identity().to_affine(),
920 pallas::Scalar::from(7u64),
921 65535,
922 true,
923 )
924 .expect_err("invalid proposal_id should be rejected before proof generation");
925
926 assert!(matches!(
927 err,
928 VoteProofBuildError::InvalidProposalId(rejected) if rejected == proposal_id
929 ));
930 }
931 }
932
933 #[test]
934 fn build_vote_proof_rejects_identity_ea_pk() {
935 let sk = test_sk();
936 let err = build_vote_proof_from_delegation(
937 &sk,
938 1,
939 BALLOT_DIVISOR,
940 test_van(),
941 test_round_id(),
942 [pallas::Base::from(0u64); VOTE_COMM_TREE_DEPTH],
943 0,
944 123,
945 1,
946 1,
947 pallas::Point::identity().to_affine(),
948 pallas::Scalar::from(7u64),
949 65535,
950 true,
951 )
952 .expect_err("identity ea_pk should be rejected before proof generation");
953
954 assert!(matches!(err, VoteProofBuildError::InvalidElectionPublicKey));
955 }
956
957 #[test]
958 fn encrypted_share_coordinates_rejects_identity_c1_point() {
959 let err = encrypted_share_coordinates(pallas::Point::identity().to_affine(), 7, "c1")
960 .expect_err("identity c1 point should be rejected");
961
962 assert!(matches!(
963 err,
964 VoteProofBuildError::InvalidEncryptedShare(msg)
965 if msg == "share 7 c1 is identity"
966 ));
967 }
968
969 #[test]
970 fn build_vote_proof_rejects_identity_c2_point() {
971 let sk = test_sk();
972 let voting_round_id = test_round_id();
973 let proposal_id = 1;
974 let proposal_authority_old_u64 = 65535;
975 let van_comm_rand = test_van();
976 let num_ballots_base = pallas::Base::from(1u64);
977
978 let fvk: FullViewingKey = (&sk).into();
979 let address = fvk.address_at(1u32, Scope::External);
980 let vpk_g_d_affine = address.g_d().to_affine();
981 let vpk_pk_d_affine = address.pk_d().inner().to_affine();
982 let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
983 let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
984
985 let vote_authority_note_old = van_integrity_hash(
986 vpk_g_d_x,
987 vpk_pk_d_x,
988 num_ballots_base,
989 voting_round_id,
990 pallas::Base::from(proposal_authority_old_u64),
991 van_comm_rand,
992 );
993 let r = derive_share_randomness(
994 &sk,
995 voting_round_id,
996 proposal_id,
997 vote_authority_note_old,
998 0,
999 );
1000 let r_scalar = base_to_scalar(r).expect("test randomness should be scalar-range");
1001 let r_inv: Option<pallas::Scalar> = r_scalar.invert().into();
1002 let ea_pk_scalar =
1003 -pallas::Scalar::from(1u64) * r_inv.expect("test randomness should be non-zero");
1004 let ea_pk = (pallas::Point::from(spend_auth_g_affine()) * ea_pk_scalar).to_affine();
1005
1006 let err = build_vote_proof_from_delegation(
1007 &sk,
1008 1,
1009 BALLOT_DIVISOR,
1010 van_comm_rand,
1011 voting_round_id,
1012 [pallas::Base::from(0u64); VOTE_COMM_TREE_DEPTH],
1013 0,
1014 123,
1015 proposal_id,
1016 1,
1017 ea_pk,
1018 pallas::Scalar::from(7u64),
1019 proposal_authority_old_u64,
1020 true,
1021 )
1022 .expect_err("crafted ea_pk should make share 0 c2 the identity");
1023
1024 assert!(matches!(
1025 err,
1026 VoteProofBuildError::InvalidEncryptedShare(msg)
1027 if msg == "share 0 c2 is identity"
1028 ));
1029 }
1030
1031 #[test]
1032 fn build_vote_proof_rejects_identity_r_vpk() {
1033 let sk = test_sk();
1034 let ea_pk =
1035 (pallas::Point::from(spend_auth_g_affine()) * pallas::Scalar::from(42u64)).to_affine();
1036 let err = build_vote_proof_from_delegation(
1037 &sk,
1038 1,
1039 BALLOT_DIVISOR,
1040 test_van(),
1041 test_round_id(),
1042 [pallas::Base::from(0u64); VOTE_COMM_TREE_DEPTH],
1043 0,
1044 123,
1045 1,
1046 1,
1047 ea_pk,
1048 -extract_vsk(&sk),
1049 65535,
1050 true,
1051 )
1052 .expect_err("alpha_v = -vsk should make r_vpk the identity");
1053
1054 assert!(matches!(
1055 err,
1056 VoteProofBuildError::InvalidRandomizedVotingPublicKey
1057 ));
1058 }
1059
1060 #[test]
1061 fn derive_share_randomness_is_deterministic() {
1062 let sk = test_sk();
1063 let round_id = test_round_id();
1064 let van = test_van();
1065 let a = derive_share_randomness(&sk, round_id, 1, van, 0);
1066 let b = derive_share_randomness(&sk, round_id, 1, van, 0);
1067 assert_eq!(a, b);
1068 }
1069
1070 #[test]
1071 fn derive_share_blind_is_deterministic() {
1072 let sk = test_sk();
1073 let round_id = test_round_id();
1074 let van = test_van();
1075 let a = derive_share_blind(&sk, round_id, 1, van, 0);
1076 let b = derive_share_blind(&sk, round_id, 1, van, 0);
1077 assert_eq!(a, b);
1078 }
1079
1080 #[test]
1081 fn derive_share_randomness_is_nonzero_valid_scalar() {
1082 let sk = test_sk();
1083 let round_id = test_round_id();
1084 let van = test_van();
1085 for i in 0..16u8 {
1086 let r = derive_share_randomness(&sk, round_id, 1, van, i);
1087 assert!(
1088 bool::from(!r.is_zero()),
1089 "r_{} must be non-zero for the circuit hardening gate",
1090 i
1091 );
1092 assert!(
1093 base_to_scalar(r).is_some(),
1094 "r_{} must be convertible to scalar",
1095 i
1096 );
1097 }
1098 }
1099
1100 #[test]
1101 fn different_share_index_gives_different_values() {
1102 let sk = test_sk();
1103 let round_id = test_round_id();
1104 let van = test_van();
1105 let r0 = derive_share_randomness(&sk, round_id, 1, van, 0);
1106 let r1 = derive_share_randomness(&sk, round_id, 1, van, 1);
1107 assert_ne!(r0, r1);
1108
1109 let b0 = derive_share_blind(&sk, round_id, 1, van, 0);
1110 let b1 = derive_share_blind(&sk, round_id, 1, van, 1);
1111 assert_ne!(b0, b1);
1112 }
1113
1114 #[test]
1115 fn different_proposal_id_gives_different_values() {
1116 let sk = test_sk();
1117 let round_id = test_round_id();
1118 let van = test_van();
1119 let r_p1 = derive_share_randomness(&sk, round_id, 1, van, 0);
1120 let r_p2 = derive_share_randomness(&sk, round_id, 2, van, 0);
1121 assert_ne!(r_p1, r_p2);
1122 }
1123
1124 #[test]
1125 fn different_round_id_gives_different_values() {
1126 let sk = test_sk();
1127 let van = test_van();
1128 let r_a = derive_share_randomness(&sk, pallas::Base::from(1u64), 1, van, 0);
1129 let r_b = derive_share_randomness(&sk, pallas::Base::from(2u64), 1, van, 0);
1130 assert_ne!(r_a, r_b);
1131 }
1132
1133 #[test]
1134 fn randomness_and_blind_differ_for_same_inputs() {
1135 let sk = test_sk();
1136 let round_id = test_round_id();
1137 let van = test_van();
1138 let r = derive_share_randomness(&sk, round_id, 1, van, 0);
1139 let b = derive_share_blind(&sk, round_id, 1, van, 0);
1140 assert_ne!(r, b, "domain separation must prevent r == blind");
1141 }
1142
1143 #[test]
1144 fn all_16_shares_are_distinct() {
1145 let sk = test_sk();
1146 let round_id = test_round_id();
1147 let van = test_van();
1148 let randoms: Vec<_> = (0..16u8)
1149 .map(|i| derive_share_randomness(&sk, round_id, 1, van, i))
1150 .collect();
1151 let blinds: Vec<_> = (0..16u8)
1152 .map(|i| derive_share_blind(&sk, round_id, 1, van, i))
1153 .collect();
1154 for i in 0..16 {
1155 for j in (i + 1)..16 {
1156 assert_ne!(randoms[i], randoms[j], "r_{} == r_{}", i, j);
1157 assert_ne!(blinds[i], blinds[j], "blind_{} == blind_{}", i, j);
1158 }
1159 }
1160 }
1161
1162 #[test]
1163 fn different_van_commitment_gives_different_values() {
1164 let sk = test_sk();
1165 let round_id = test_round_id();
1166 let van_a = pallas::Base::from(0xAAAA_u64);
1167 let van_b = pallas::Base::from(0xBBBB_u64);
1168 for i in 0..16u8 {
1169 let r_a = derive_share_randomness(&sk, round_id, 1, van_a, i);
1170 let r_b = derive_share_randomness(&sk, round_id, 1, van_b, i);
1171 assert_ne!(r_a, r_b, "r_{} must differ across VANs", i);
1172
1173 let b_a = derive_share_blind(&sk, round_id, 1, van_a, i);
1174 let b_b = derive_share_blind(&sk, round_id, 1, van_b, i);
1175 assert_ne!(b_a, b_b, "blind_{} must differ across VANs", i);
1176 }
1177 }
1178
1179 fn show(label: &str, shares: &[u64; 16]) {
1191 let parts: Vec<String> = shares
1192 .iter()
1193 .map(|&v| {
1194 if v == 0 {
1195 "0".into()
1196 } else if v >= 1_000_000 {
1197 format!("{}M", v / 1_000_000)
1198 } else if v >= 1_000 {
1199 format!("{}K", v / 1_000)
1200 } else {
1201 format!("{}", v)
1202 }
1203 })
1204 .collect();
1205 std::eprintln!(" {}: [{}]", label, parts.join(", "));
1206 }
1207
1208 #[test]
1209 fn denom_split_zero_ballots() {
1210 let sk = test_sk();
1213 let rid = test_round_id();
1214 let van = test_van();
1215 let shares = denomination_split(0, &sk, rid, 1, van);
1216 show("0 ballots", &shares);
1217 assert_eq!(shares, [0; 16]);
1218 }
1219
1220 #[test]
1221 fn denom_split_single_ballot() {
1222 let sk = test_sk();
1225 let rid = test_round_id();
1226 let van = test_van();
1227 let shares = denomination_split(1, &sk, rid, 1, van);
1228 show("1 ballot (0.125 ZEC)", &shares);
1229 assert_eq!(shares[0], 1);
1230 for i in 1..16 {
1231 assert_eq!(shares[i], 0);
1232 }
1233 }
1234
1235 #[test]
1236 fn denom_split_sub_zec() {
1237 let sk = test_sk();
1240 let rid = test_round_id();
1241 let van = test_van();
1242 let shares = denomination_split(4, &sk, rid, 1, van);
1243 show("4 ballots (0.5 ZEC)", &shares);
1244 assert_eq!(shares[0..4], [1; 4]);
1245 for i in 4..16 {
1246 assert_eq!(shares[i], 0);
1247 }
1248 }
1249
1250 #[test]
1251 fn denom_split_one_zec() {
1252 let sk = test_sk();
1255 let rid = test_round_id();
1256 let van = test_van();
1257 let shares = denomination_split(8, &sk, rid, 1, van);
1258 show("8 ballots (1 ZEC)", &shares);
1259 assert_eq!(shares[0..8], [1; 8]);
1260 for i in 8..16 {
1261 assert_eq!(shares[i], 0);
1262 }
1263 }
1264
1265 #[test]
1266 fn denom_split_small_balance() {
1267 let sk = test_sk();
1270 let rid = test_round_id();
1271 let van = test_van();
1272 let shares = denomination_split(50, &sk, rid, 1, van);
1273 show("50 ballots (6.25 ZEC)", &shares);
1274 assert_eq!(shares[0..5], [10; 5]);
1275 for i in 5..16 {
1276 assert_eq!(shares[i], 0);
1277 }
1278 }
1279
1280 #[test]
1281 fn denom_split_all_denoms_exact() {
1282 let sk = test_sk();
1285 let rid = test_round_id();
1286 let van = test_van();
1287 let shares = denomination_split(11_111, &sk, rid, 1, van);
1288 show("11,111 ballots (1,388.9 ZEC)", &shares);
1289 assert_eq!(shares[0], 10_000);
1290 assert_eq!(shares[1], 1_000);
1291 assert_eq!(shares[2], 100);
1292 assert_eq!(shares[3], 10);
1293 assert_eq!(shares[4], 1);
1294 for i in 5..16 {
1295 assert_eq!(shares[i], 0);
1296 }
1297 }
1298
1299 #[test]
1300 fn denom_split_medium_holder_with_remainder() {
1301 let sk = test_sk();
1306 let rid = test_round_id();
1307 let van = test_van();
1308 let shares = denomination_split(4_800, &sk, rid, 1, van);
1309 show("4,800 ballots (600 ZEC)", &shares);
1310 assert_eq!(shares[0..4], [1_000; 4]);
1311 assert_eq!(shares[4..9], [100; 5]);
1312 let remainder_sum: u64 = shares[9..16].iter().sum();
1313 assert_eq!(remainder_sum, 300);
1314 for i in 9..16 {
1315 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1316 }
1317 assert_eq!(shares.iter().sum::<u64>(), 4_800);
1318 }
1319
1320 #[test]
1321 fn denom_split_high_hamming_weight() {
1322 let sk = test_sk();
1327 let rid = test_round_id();
1328 let van = test_van();
1329 let shares = denomination_split(999, &sk, rid, 1, van);
1330 show("999 ballots (124.875 ZEC)", &shares);
1331 assert_eq!(shares[0..9], [100; 9]);
1332 let remainder_sum: u64 = shares[9..16].iter().sum();
1333 assert_eq!(remainder_sum, 99);
1334 for i in 9..16 {
1335 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1336 }
1337 }
1338
1339 #[test]
1340 fn denom_split_exact_denomination_match() {
1341 let sk = test_sk();
1344 let rid = test_round_id();
1345 let van = test_van();
1346 let shares = denomination_split(3_000_000, &sk, rid, 1, van);
1347 show("3M ballots (375 ZEC)", &shares);
1348 assert_eq!(shares[0..3], [1_000_000; 3]);
1349 for i in 3..16 {
1350 assert_eq!(shares[i], 0);
1351 }
1352 }
1353
1354 #[test]
1355 fn denom_split_8m_ballots() {
1356 let sk = test_sk();
1359 let rid = test_round_id();
1360 let van = test_van();
1361 let shares = denomination_split(8_000_000, &sk, rid, 1, van);
1362 show("8M ballots (1M ZEC)", &shares);
1363 assert_eq!(shares[0..8], [1_000_000; 8]);
1364 for i in 8..16 {
1365 assert_eq!(shares[i], 0);
1366 }
1367 }
1368
1369 #[test]
1370 fn denom_split_fills_all_9_denom_slots() {
1371 let sk = test_sk();
1374 let rid = test_round_id();
1375 let van = test_van();
1376 let shares = denomination_split(90_000_000, &sk, rid, 1, van);
1377 show("90M ballots (11.25M ZEC)", &shares);
1378 assert_eq!(shares[0..9], [10_000_000; 9]);
1379 for i in 9..16 {
1380 assert_eq!(shares[i], 0);
1381 }
1382 }
1383
1384 #[test]
1385 fn denom_split_overflow_into_remainder() {
1386 let sk = test_sk();
1391 let rid = test_round_id();
1392 let van = test_van();
1393 let shares = denomination_split(100_000_000, &sk, rid, 1, van);
1394 show("100M ballots (12.5M ZEC)", &shares);
1395 assert_eq!(shares[0..9], [10_000_000; 9]);
1396 let remainder_sum: u64 = shares[9..16].iter().sum();
1397 assert_eq!(remainder_sum, 10_000_000);
1398 for i in 9..16 {
1399 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1400 }
1401 }
1402
1403 #[test]
1404 fn denom_split_mixed_with_remainder() {
1405 let sk = test_sk();
1409 let rid = test_round_id();
1410 let van = test_van();
1411 let shares = denomination_split(1_234_567, &sk, rid, 1, van);
1412 show("1,234,567 ballots (154K ZEC)", &shares);
1413 assert_eq!(shares[0], 1_000_000);
1414 assert_eq!(shares[1..3], [100_000; 2]);
1415 assert_eq!(shares[3..6], [10_000; 3]);
1416 assert_eq!(shares[6..9], [1_000; 3]);
1417 let remainder_sum: u64 = shares[9..16].iter().sum();
1418 assert_eq!(remainder_sum, 1_567);
1419 assert_eq!(shares.iter().sum::<u64>(), 1_234_567);
1420 }
1421
1422 #[test]
1423 fn denom_split_small_remainder_fewer_than_free_slots() {
1424 let sk = test_sk();
1428 let rid = test_round_id();
1429 let van = test_van();
1430 let shares = denomination_split(10_000_003, &sk, rid, 1, van);
1431 show("10,000,003 ballots", &shares);
1432 assert_eq!(shares[0], 10_000_000);
1433 let remainder_sum: u64 = shares[1..16].iter().sum();
1434 assert_eq!(remainder_sum, 3);
1435 assert_eq!(shares.iter().sum::<u64>(), 10_000_003);
1436 }
1437
1438 #[test]
1441 fn denom_split_sum_invariant() {
1442 let sk = test_sk();
1443 let rid = test_round_id();
1444 let van = test_van();
1445 let test_values: [u64; 14] = [
1446 0,
1447 1,
1448 50,
1449 99,
1450 100,
1451 999,
1452 1_000,
1453 10_000,
1454 100_000,
1455 1_000_000,
1456 8_234_567,
1457 20_000_000,
1458 80_000_000,
1459 168_000_000,
1460 ];
1461 for &v in &test_values {
1462 let shares = denomination_split(v, &sk, rid, 1, van);
1463 assert_eq!(
1464 shares.iter().sum::<u64>(),
1465 v,
1466 "sum invariant violated for {}",
1467 v
1468 );
1469 }
1470 }
1471
1472 #[test]
1473 fn denom_split_all_shares_in_range() {
1474 let sk = test_sk();
1475 let rid = test_round_id();
1476 let van = test_van();
1477 let test_values: [u64; 8] = [
1478 1,
1479 10_000,
1480 1_000_000,
1481 8_234_567,
1482 15_000_000,
1483 20_000_000,
1484 80_000_000,
1485 168_000_000,
1486 ];
1487 for &v in &test_values {
1488 let shares = denomination_split(v, &sk, rid, 1, van);
1489 for (i, &s) in shares.iter().enumerate() {
1490 assert!(
1491 s < (1u64 << 30),
1492 "share {} = {} exceeds 2^30 for {}",
1493 i,
1494 s,
1495 v
1496 );
1497 }
1498 }
1499 }
1500
1501 #[test]
1504 fn remainder_is_deterministic() {
1505 let sk = test_sk();
1506 let rid = test_round_id();
1507 let van = test_van();
1508 let a = denomination_split(999, &sk, rid, 1, van);
1509 let b = denomination_split(999, &sk, rid, 1, van);
1510 assert_eq!(a, b);
1511 }
1512
1513 #[test]
1514 fn remainder_differs_across_proposals() {
1515 let sk = test_sk();
1517 let rid = test_round_id();
1518 let van = test_van();
1519 let a = denomination_split(999, &sk, rid, 1, van);
1520 let b = denomination_split(999, &sk, rid, 2, van);
1521 show("999 ballots, proposal 1", &a);
1522 show("999 ballots, proposal 2", &b);
1523 assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
1524 assert_ne!(
1525 a[9..16],
1526 b[9..16],
1527 "remainder should differ across proposals"
1528 );
1529 }
1530
1531 #[test]
1532 fn remainder_differs_across_vans() {
1533 let sk = test_sk();
1535 let rid = test_round_id();
1536 let van_a = pallas::Base::from(0xAAAA_u64);
1537 let van_b = pallas::Base::from(0xBBBB_u64);
1538 let a = denomination_split(999, &sk, rid, 1, van_a);
1539 let b = denomination_split(999, &sk, rid, 1, van_b);
1540 show("999 ballots, VAN A", &a);
1541 show("999 ballots, VAN B", &b);
1542 assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
1543 assert_ne!(a[9..16], b[9..16], "remainder should differ across VANs");
1544 }
1545
1546 #[test]
1549 fn shuffle_preserves_sum() {
1550 let sk = test_sk();
1551 let round_id = test_round_id();
1552 let van = test_van();
1553 let mut shares = denomination_split(8_234_567, &sk, round_id, 1, van);
1554 let sum_before = shares.iter().sum::<u64>();
1555 deterministic_shuffle(&mut shares, &sk, round_id, 1, van);
1556 assert_eq!(shares.iter().sum::<u64>(), sum_before);
1557 }
1558
1559 #[test]
1560 fn shuffle_preserves_multiset() {
1561 let sk = test_sk();
1562 let round_id = test_round_id();
1563 let van = test_van();
1564 let original = denomination_split(4_800, &sk, round_id, 1, van);
1565 let mut shuffled = original;
1566 deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
1567 let mut sorted_orig = original;
1568 sorted_orig.sort();
1569 let mut sorted_shuf = shuffled;
1570 sorted_shuf.sort();
1571 assert_eq!(sorted_orig, sorted_shuf, "shuffle must be a permutation");
1572 }
1573
1574 #[test]
1575 fn shuffle_is_deterministic() {
1576 let sk = test_sk();
1577 let round_id = test_round_id();
1578 let van = test_van();
1579 let mut a = denomination_split(4_800, &sk, round_id, 1, van);
1580 let mut b = denomination_split(4_800, &sk, round_id, 1, van);
1581 deterministic_shuffle(&mut a, &sk, round_id, 1, van);
1582 deterministic_shuffle(&mut b, &sk, round_id, 1, van);
1583 assert_eq!(a, b, "same inputs must produce same permutation");
1584 }
1585
1586 #[test]
1587 fn shuffle_differs_across_proposals() {
1588 let sk = test_sk();
1589 let round_id = test_round_id();
1590 let van = test_van();
1591 let mut a = denomination_split(4_800, &sk, round_id, 1, van);
1592 let mut b = denomination_split(4_800, &sk, round_id, 1, van);
1593 deterministic_shuffle(&mut a, &sk, round_id, 1, van);
1594 deterministic_shuffle(&mut b, &sk, round_id, 2, van);
1595 assert_ne!(
1596 a, b,
1597 "different proposals should produce different permutations"
1598 );
1599 }
1600
1601 #[test]
1602 fn shuffle_differs_across_vans() {
1603 let sk = test_sk();
1604 let round_id = test_round_id();
1605 let van_a = pallas::Base::from(0xAAAA_u64);
1606 let van_b = pallas::Base::from(0xBBBB_u64);
1607 let mut a = denomination_split(4_800, &sk, round_id, 1, van_a);
1608 let mut b = denomination_split(4_800, &sk, round_id, 1, van_b);
1609 deterministic_shuffle(&mut a, &sk, round_id, 1, van_a);
1610 deterministic_shuffle(&mut b, &sk, round_id, 1, van_b);
1611 assert_ne!(a, b, "different VANs should produce different permutations");
1612 }
1613
1614 #[test]
1615 fn shuffle_actually_reorders() {
1616 let sk = test_sk();
1617 let round_id = test_round_id();
1618 let van = test_van();
1619 let original = denomination_split(4_800, &sk, round_id, 1, van);
1620 let mut shuffled = original;
1621 deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
1622 assert_ne!(
1623 original, shuffled,
1624 "shuffle should reorder (vanishingly unlikely to be identity for 12 non-zero shares)"
1625 );
1626 }
1627
1628 #[test]
1629 fn prove_error_maps_into_build_error() {
1630 let err =
1631 VoteProofBuildError::from(ProveError::Halo2(halo2_proofs::plonk::Error::Synthesis));
1632
1633 assert!(matches!(err, VoteProofBuildError::Prove(_)));
1634 }
1635}