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