1use std::{string::String, vec::Vec};
12
13use ff::{Field, FromUniformBytes, PrimeField};
14use group::{Curve, GroupEncoding};
15use halo2_proofs::circuit::Value;
16use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey};
17use pasta_curves::{
18 arithmetic::{Coordinates, CurveAffine},
19 pallas,
20};
21
22use super::{
23 circuit::{
24 van_integrity_hash, van_nullifier_hash, vote_commitment_hash, Circuit, Instance,
25 MAX_PROPOSAL_ID,
26 },
27 prove::create_vote_proof,
28};
29use crate::{
30 domain_tags,
31 gadgets::elgamal::{base_to_scalar, spend_auth_g_affine},
32 params::{BALLOT_DIVISOR, VOTE_COMM_TREE_DEPTH},
33 shares_hash::{share_commitment, shares_hash},
34 ProveError,
35};
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 ak_point = (spend_auth_g_affine() * ask_raw).to_affine();
343 let ak_bytes = ak_point.to_bytes();
344
345 if (ak_bytes.as_ref()[31] >> 7) == 1 {
347 -ask_raw
348 } else {
349 ask_raw
350 }
351}
352
353fn vote_share_prf(
364 sk: &SpendingKey,
365 domain: u8,
366 round_id: pallas::Base,
367 proposal_id: u64,
368 van_commitment: pallas::Base,
369 share_index: u8,
370) -> [u8; 64] {
371 *blake2b_simd::Params::new()
372 .hash_length(64)
373 .personal(domain_tags::VOTE_PRF_PERSONALIZATION)
374 .to_state()
375 .update(sk.to_bytes())
376 .update(&[domain])
377 .update(&round_id.to_repr())
378 .update(&proposal_id.to_le_bytes())
379 .update(&van_commitment.to_repr())
380 .update(&[share_index])
381 .finalize()
382 .as_array()
383}
384
385fn derive_share_randomness(
391 sk: &SpendingKey,
392 round_id: pallas::Base,
393 proposal_id: u64,
394 van_commitment: pallas::Base,
395 share_index: u8,
396) -> pallas::Base {
397 let hash = vote_share_prf(
398 sk,
399 domain_tags::VOTE_PRF_DOMAIN_ELGAMAL,
400 round_id,
401 proposal_id,
402 van_commitment,
403 share_index,
404 );
405 let r = pallas::Base::from_uniform_bytes(&hash);
406 if bool::from(r.is_zero()) {
407 return pallas::Base::one();
410 }
411 debug_assert!(base_to_scalar(r).is_some(), "p < q guarantees Base→Scalar");
412 r
413}
414
415fn derive_share_blind(
417 sk: &SpendingKey,
418 round_id: pallas::Base,
419 proposal_id: u64,
420 van_commitment: pallas::Base,
421 share_index: u8,
422) -> pallas::Base {
423 let hash = vote_share_prf(
424 sk,
425 domain_tags::VOTE_PRF_DOMAIN_BLIND,
426 round_id,
427 proposal_id,
428 van_commitment,
429 share_index,
430 );
431 pallas::Base::from_uniform_bytes(&hash)
432}
433
434fn deterministic_shuffle(
448 shares: &mut [u64; NUM_SHARES],
449 sk: &SpendingKey,
450 round_id: pallas::Base,
451 proposal_id: u64,
452 van_commitment: pallas::Base,
453) {
454 let seed = vote_share_prf(
458 sk,
459 domain_tags::VOTE_PRF_DOMAIN_SHUFFLE,
460 round_id,
461 proposal_id,
462 van_commitment,
463 0,
464 );
465 for i in (1..NUM_SHARES).rev() {
466 let byte_offset = (NUM_SHARES - 1 - i) * 4;
470 let rand_bytes: [u8; 4] = seed[byte_offset..byte_offset + 4]
471 .try_into()
472 .expect("64-byte seed has room for 15 × 4-byte draws");
473 let j = (u32::from_le_bytes(rand_bytes) as usize) % (i + 1);
474 shares.swap(i, j);
475 }
476}
477
478pub 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
557 let vsk = extract_vsk(sk);
560 let fvk: FullViewingKey = sk.into();
561 let vsk_nk = fvk.nk().inner();
562 let rivk_v = fvk.rivk(Scope::External).inner();
563
564 let address = fvk.address_at(address_index, Scope::External);
565 let vpk_g_d = address.g_d();
566 let vpk_pk_d = address.pk_d().inner();
567 let vpk_g_d_affine = vpk_g_d.to_affine();
568 let vpk_pk_d_affine = vpk_pk_d.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 = (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 = (*vpk_g_d * 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 = 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 * 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 r_vpk = (spend_auth_g_affine() * (vsk + 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 crate::protocol_hash::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 = (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 = (spend_auth_g_affine() * pallas::Scalar::from(42u64)).to_affine();
1034 let err = build_vote_proof_from_delegation(
1035 &sk,
1036 1,
1037 BALLOT_DIVISOR,
1038 test_van(),
1039 test_round_id(),
1040 [pallas::Base::from(0u64); VOTE_COMM_TREE_DEPTH],
1041 0,
1042 123,
1043 1,
1044 1,
1045 ea_pk,
1046 -extract_vsk(&sk),
1047 65535,
1048 true,
1049 )
1050 .expect_err("alpha_v = -vsk should make r_vpk the identity");
1051
1052 assert!(matches!(
1053 err,
1054 VoteProofBuildError::InvalidRandomizedVotingPublicKey
1055 ));
1056 }
1057
1058 #[test]
1059 fn derive_share_randomness_is_deterministic() {
1060 let sk = test_sk();
1061 let round_id = test_round_id();
1062 let van = test_van();
1063 let a = derive_share_randomness(&sk, round_id, 1, van, 0);
1064 let b = derive_share_randomness(&sk, round_id, 1, van, 0);
1065 assert_eq!(a, b);
1066 }
1067
1068 #[test]
1069 fn derive_share_blind_is_deterministic() {
1070 let sk = test_sk();
1071 let round_id = test_round_id();
1072 let van = test_van();
1073 let a = derive_share_blind(&sk, round_id, 1, van, 0);
1074 let b = derive_share_blind(&sk, round_id, 1, van, 0);
1075 assert_eq!(a, b);
1076 }
1077
1078 #[test]
1079 fn derive_share_randomness_is_nonzero_valid_scalar() {
1080 let sk = test_sk();
1081 let round_id = test_round_id();
1082 let van = test_van();
1083 for i in 0..16u8 {
1084 let r = derive_share_randomness(&sk, round_id, 1, van, i);
1085 assert!(
1086 bool::from(!r.is_zero()),
1087 "r_{} must be non-zero for the circuit hardening gate",
1088 i
1089 );
1090 assert!(
1091 base_to_scalar(r).is_some(),
1092 "r_{} must be convertible to scalar",
1093 i
1094 );
1095 }
1096 }
1097
1098 #[test]
1099 fn different_share_index_gives_different_values() {
1100 let sk = test_sk();
1101 let round_id = test_round_id();
1102 let van = test_van();
1103 let r0 = derive_share_randomness(&sk, round_id, 1, van, 0);
1104 let r1 = derive_share_randomness(&sk, round_id, 1, van, 1);
1105 assert_ne!(r0, r1);
1106
1107 let b0 = derive_share_blind(&sk, round_id, 1, van, 0);
1108 let b1 = derive_share_blind(&sk, round_id, 1, van, 1);
1109 assert_ne!(b0, b1);
1110 }
1111
1112 #[test]
1113 fn different_proposal_id_gives_different_values() {
1114 let sk = test_sk();
1115 let round_id = test_round_id();
1116 let van = test_van();
1117 let r_p1 = derive_share_randomness(&sk, round_id, 1, van, 0);
1118 let r_p2 = derive_share_randomness(&sk, round_id, 2, van, 0);
1119 assert_ne!(r_p1, r_p2);
1120 }
1121
1122 #[test]
1123 fn different_round_id_gives_different_values() {
1124 let sk = test_sk();
1125 let van = test_van();
1126 let r_a = derive_share_randomness(&sk, pallas::Base::from(1u64), 1, van, 0);
1127 let r_b = derive_share_randomness(&sk, pallas::Base::from(2u64), 1, van, 0);
1128 assert_ne!(r_a, r_b);
1129 }
1130
1131 #[test]
1132 fn randomness_and_blind_differ_for_same_inputs() {
1133 let sk = test_sk();
1134 let round_id = test_round_id();
1135 let van = test_van();
1136 let r = derive_share_randomness(&sk, round_id, 1, van, 0);
1137 let b = derive_share_blind(&sk, round_id, 1, van, 0);
1138 assert_ne!(r, b, "domain separation must prevent r == blind");
1139 }
1140
1141 #[test]
1142 fn all_16_shares_are_distinct() {
1143 let sk = test_sk();
1144 let round_id = test_round_id();
1145 let van = test_van();
1146 let randoms: Vec<_> = (0..16u8)
1147 .map(|i| derive_share_randomness(&sk, round_id, 1, van, i))
1148 .collect();
1149 let blinds: Vec<_> = (0..16u8)
1150 .map(|i| derive_share_blind(&sk, round_id, 1, van, i))
1151 .collect();
1152 for i in 0..16 {
1153 for j in (i + 1)..16 {
1154 assert_ne!(randoms[i], randoms[j], "r_{} == r_{}", i, j);
1155 assert_ne!(blinds[i], blinds[j], "blind_{} == blind_{}", i, j);
1156 }
1157 }
1158 }
1159
1160 #[test]
1161 fn different_van_commitment_gives_different_values() {
1162 let sk = test_sk();
1163 let round_id = test_round_id();
1164 let van_a = pallas::Base::from(0xAAAA_u64);
1165 let van_b = pallas::Base::from(0xBBBB_u64);
1166 for i in 0..16u8 {
1167 let r_a = derive_share_randomness(&sk, round_id, 1, van_a, i);
1168 let r_b = derive_share_randomness(&sk, round_id, 1, van_b, i);
1169 assert_ne!(r_a, r_b, "r_{} must differ across VANs", i);
1170
1171 let b_a = derive_share_blind(&sk, round_id, 1, van_a, i);
1172 let b_b = derive_share_blind(&sk, round_id, 1, van_b, i);
1173 assert_ne!(b_a, b_b, "blind_{} must differ across VANs", i);
1174 }
1175 }
1176
1177 fn show(label: &str, shares: &[u64; 16]) {
1189 let parts: Vec<String> = shares
1190 .iter()
1191 .map(|&v| {
1192 if v == 0 {
1193 "0".into()
1194 } else if v >= 1_000_000 {
1195 format!("{}M", v / 1_000_000)
1196 } else if v >= 1_000 {
1197 format!("{}K", v / 1_000)
1198 } else {
1199 format!("{}", v)
1200 }
1201 })
1202 .collect();
1203 std::eprintln!(" {}: [{}]", label, parts.join(", "));
1204 }
1205
1206 #[test]
1207 fn denom_split_zero_ballots() {
1208 let sk = test_sk();
1211 let rid = test_round_id();
1212 let van = test_van();
1213 let shares = denomination_split(0, &sk, rid, 1, van);
1214 show("0 ballots", &shares);
1215 assert_eq!(shares, [0; 16]);
1216 }
1217
1218 #[test]
1219 fn denom_split_single_ballot() {
1220 let sk = test_sk();
1223 let rid = test_round_id();
1224 let van = test_van();
1225 let shares = denomination_split(1, &sk, rid, 1, van);
1226 show("1 ballot (0.125 ZEC)", &shares);
1227 assert_eq!(shares[0], 1);
1228 for i in 1..16 {
1229 assert_eq!(shares[i], 0);
1230 }
1231 }
1232
1233 #[test]
1234 fn denom_split_sub_zec() {
1235 let sk = test_sk();
1238 let rid = test_round_id();
1239 let van = test_van();
1240 let shares = denomination_split(4, &sk, rid, 1, van);
1241 show("4 ballots (0.5 ZEC)", &shares);
1242 assert_eq!(shares[0..4], [1; 4]);
1243 for i in 4..16 {
1244 assert_eq!(shares[i], 0);
1245 }
1246 }
1247
1248 #[test]
1249 fn denom_split_one_zec() {
1250 let sk = test_sk();
1253 let rid = test_round_id();
1254 let van = test_van();
1255 let shares = denomination_split(8, &sk, rid, 1, van);
1256 show("8 ballots (1 ZEC)", &shares);
1257 assert_eq!(shares[0..8], [1; 8]);
1258 for i in 8..16 {
1259 assert_eq!(shares[i], 0);
1260 }
1261 }
1262
1263 #[test]
1264 fn denom_split_small_balance() {
1265 let sk = test_sk();
1268 let rid = test_round_id();
1269 let van = test_van();
1270 let shares = denomination_split(50, &sk, rid, 1, van);
1271 show("50 ballots (6.25 ZEC)", &shares);
1272 assert_eq!(shares[0..5], [10; 5]);
1273 for i in 5..16 {
1274 assert_eq!(shares[i], 0);
1275 }
1276 }
1277
1278 #[test]
1279 fn denom_split_all_denoms_exact() {
1280 let sk = test_sk();
1283 let rid = test_round_id();
1284 let van = test_van();
1285 let shares = denomination_split(11_111, &sk, rid, 1, van);
1286 show("11,111 ballots (1,388.9 ZEC)", &shares);
1287 assert_eq!(shares[0], 10_000);
1288 assert_eq!(shares[1], 1_000);
1289 assert_eq!(shares[2], 100);
1290 assert_eq!(shares[3], 10);
1291 assert_eq!(shares[4], 1);
1292 for i in 5..16 {
1293 assert_eq!(shares[i], 0);
1294 }
1295 }
1296
1297 #[test]
1298 fn denom_split_medium_holder_with_remainder() {
1299 let sk = test_sk();
1304 let rid = test_round_id();
1305 let van = test_van();
1306 let shares = denomination_split(4_800, &sk, rid, 1, van);
1307 show("4,800 ballots (600 ZEC)", &shares);
1308 assert_eq!(shares[0..4], [1_000; 4]);
1309 assert_eq!(shares[4..9], [100; 5]);
1310 let remainder_sum: u64 = shares[9..16].iter().sum();
1311 assert_eq!(remainder_sum, 300);
1312 for i in 9..16 {
1313 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1314 }
1315 assert_eq!(shares.iter().sum::<u64>(), 4_800);
1316 }
1317
1318 #[test]
1319 fn denom_split_high_hamming_weight() {
1320 let sk = test_sk();
1325 let rid = test_round_id();
1326 let van = test_van();
1327 let shares = denomination_split(999, &sk, rid, 1, van);
1328 show("999 ballots (124.875 ZEC)", &shares);
1329 assert_eq!(shares[0..9], [100; 9]);
1330 let remainder_sum: u64 = shares[9..16].iter().sum();
1331 assert_eq!(remainder_sum, 99);
1332 for i in 9..16 {
1333 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1334 }
1335 }
1336
1337 #[test]
1338 fn denom_split_exact_denomination_match() {
1339 let sk = test_sk();
1342 let rid = test_round_id();
1343 let van = test_van();
1344 let shares = denomination_split(3_000_000, &sk, rid, 1, van);
1345 show("3M ballots (375 ZEC)", &shares);
1346 assert_eq!(shares[0..3], [1_000_000; 3]);
1347 for i in 3..16 {
1348 assert_eq!(shares[i], 0);
1349 }
1350 }
1351
1352 #[test]
1353 fn denom_split_8m_ballots() {
1354 let sk = test_sk();
1357 let rid = test_round_id();
1358 let van = test_van();
1359 let shares = denomination_split(8_000_000, &sk, rid, 1, van);
1360 show("8M ballots (1M ZEC)", &shares);
1361 assert_eq!(shares[0..8], [1_000_000; 8]);
1362 for i in 8..16 {
1363 assert_eq!(shares[i], 0);
1364 }
1365 }
1366
1367 #[test]
1368 fn denom_split_fills_all_9_denom_slots() {
1369 let sk = test_sk();
1372 let rid = test_round_id();
1373 let van = test_van();
1374 let shares = denomination_split(90_000_000, &sk, rid, 1, van);
1375 show("90M ballots (11.25M ZEC)", &shares);
1376 assert_eq!(shares[0..9], [10_000_000; 9]);
1377 for i in 9..16 {
1378 assert_eq!(shares[i], 0);
1379 }
1380 }
1381
1382 #[test]
1383 fn denom_split_overflow_into_remainder() {
1384 let sk = test_sk();
1389 let rid = test_round_id();
1390 let van = test_van();
1391 let shares = denomination_split(100_000_000, &sk, rid, 1, van);
1392 show("100M ballots (12.5M ZEC)", &shares);
1393 assert_eq!(shares[0..9], [10_000_000; 9]);
1394 let remainder_sum: u64 = shares[9..16].iter().sum();
1395 assert_eq!(remainder_sum, 10_000_000);
1396 for i in 9..16 {
1397 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1398 }
1399 }
1400
1401 #[test]
1402 fn denom_split_mixed_with_remainder() {
1403 let sk = test_sk();
1407 let rid = test_round_id();
1408 let van = test_van();
1409 let shares = denomination_split(1_234_567, &sk, rid, 1, van);
1410 show("1,234,567 ballots (154K ZEC)", &shares);
1411 assert_eq!(shares[0], 1_000_000);
1412 assert_eq!(shares[1..3], [100_000; 2]);
1413 assert_eq!(shares[3..6], [10_000; 3]);
1414 assert_eq!(shares[6..9], [1_000; 3]);
1415 let remainder_sum: u64 = shares[9..16].iter().sum();
1416 assert_eq!(remainder_sum, 1_567);
1417 assert_eq!(shares.iter().sum::<u64>(), 1_234_567);
1418 }
1419
1420 #[test]
1421 fn denom_split_small_remainder_fewer_than_free_slots() {
1422 let sk = test_sk();
1426 let rid = test_round_id();
1427 let van = test_van();
1428 let shares = denomination_split(10_000_003, &sk, rid, 1, van);
1429 show("10,000,003 ballots", &shares);
1430 assert_eq!(shares[0], 10_000_000);
1431 let remainder_sum: u64 = shares[1..16].iter().sum();
1432 assert_eq!(remainder_sum, 3);
1433 assert_eq!(shares.iter().sum::<u64>(), 10_000_003);
1434 }
1435
1436 #[test]
1439 fn denom_split_sum_invariant() {
1440 let sk = test_sk();
1441 let rid = test_round_id();
1442 let van = test_van();
1443 let test_values: [u64; 14] = [
1444 0,
1445 1,
1446 50,
1447 99,
1448 100,
1449 999,
1450 1_000,
1451 10_000,
1452 100_000,
1453 1_000_000,
1454 8_234_567,
1455 20_000_000,
1456 80_000_000,
1457 168_000_000,
1458 ];
1459 for &v in &test_values {
1460 let shares = denomination_split(v, &sk, rid, 1, van);
1461 assert_eq!(
1462 shares.iter().sum::<u64>(),
1463 v,
1464 "sum invariant violated for {}",
1465 v
1466 );
1467 }
1468 }
1469
1470 #[test]
1471 fn denom_split_all_shares_in_range() {
1472 let sk = test_sk();
1473 let rid = test_round_id();
1474 let van = test_van();
1475 let test_values: [u64; 8] = [
1476 1,
1477 10_000,
1478 1_000_000,
1479 8_234_567,
1480 15_000_000,
1481 20_000_000,
1482 80_000_000,
1483 168_000_000,
1484 ];
1485 for &v in &test_values {
1486 let shares = denomination_split(v, &sk, rid, 1, van);
1487 for (i, &s) in shares.iter().enumerate() {
1488 assert!(
1489 s < (1u64 << 30),
1490 "share {} = {} exceeds 2^30 for {}",
1491 i,
1492 s,
1493 v
1494 );
1495 }
1496 }
1497 }
1498
1499 #[test]
1502 fn remainder_is_deterministic() {
1503 let sk = test_sk();
1504 let rid = test_round_id();
1505 let van = test_van();
1506 let a = denomination_split(999, &sk, rid, 1, van);
1507 let b = denomination_split(999, &sk, rid, 1, van);
1508 assert_eq!(a, b);
1509 }
1510
1511 #[test]
1512 fn remainder_differs_across_proposals() {
1513 let sk = test_sk();
1515 let rid = test_round_id();
1516 let van = test_van();
1517 let a = denomination_split(999, &sk, rid, 1, van);
1518 let b = denomination_split(999, &sk, rid, 2, van);
1519 show("999 ballots, proposal 1", &a);
1520 show("999 ballots, proposal 2", &b);
1521 assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
1522 assert_ne!(
1523 a[9..16],
1524 b[9..16],
1525 "remainder should differ across proposals"
1526 );
1527 }
1528
1529 #[test]
1530 fn remainder_differs_across_vans() {
1531 let sk = test_sk();
1533 let rid = test_round_id();
1534 let van_a = pallas::Base::from(0xAAAA_u64);
1535 let van_b = pallas::Base::from(0xBBBB_u64);
1536 let a = denomination_split(999, &sk, rid, 1, van_a);
1537 let b = denomination_split(999, &sk, rid, 1, van_b);
1538 show("999 ballots, VAN A", &a);
1539 show("999 ballots, VAN B", &b);
1540 assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
1541 assert_ne!(a[9..16], b[9..16], "remainder should differ across VANs");
1542 }
1543
1544 #[test]
1547 fn shuffle_preserves_sum() {
1548 let sk = test_sk();
1549 let round_id = test_round_id();
1550 let van = test_van();
1551 let mut shares = denomination_split(8_234_567, &sk, round_id, 1, van);
1552 let sum_before = shares.iter().sum::<u64>();
1553 deterministic_shuffle(&mut shares, &sk, round_id, 1, van);
1554 assert_eq!(shares.iter().sum::<u64>(), sum_before);
1555 }
1556
1557 #[test]
1558 fn shuffle_preserves_multiset() {
1559 let sk = test_sk();
1560 let round_id = test_round_id();
1561 let van = test_van();
1562 let original = denomination_split(4_800, &sk, round_id, 1, van);
1563 let mut shuffled = original;
1564 deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
1565 let mut sorted_orig = original;
1566 sorted_orig.sort();
1567 let mut sorted_shuf = shuffled;
1568 sorted_shuf.sort();
1569 assert_eq!(sorted_orig, sorted_shuf, "shuffle must be a permutation");
1570 }
1571
1572 #[test]
1573 fn shuffle_is_deterministic() {
1574 let sk = test_sk();
1575 let round_id = test_round_id();
1576 let van = test_van();
1577 let mut a = denomination_split(4_800, &sk, round_id, 1, van);
1578 let mut b = denomination_split(4_800, &sk, round_id, 1, van);
1579 deterministic_shuffle(&mut a, &sk, round_id, 1, van);
1580 deterministic_shuffle(&mut b, &sk, round_id, 1, van);
1581 assert_eq!(a, b, "same inputs must produce same permutation");
1582 }
1583
1584 #[test]
1585 fn shuffle_differs_across_proposals() {
1586 let sk = test_sk();
1587 let round_id = test_round_id();
1588 let van = test_van();
1589 let mut a = denomination_split(4_800, &sk, round_id, 1, van);
1590 let mut b = denomination_split(4_800, &sk, round_id, 1, van);
1591 deterministic_shuffle(&mut a, &sk, round_id, 1, van);
1592 deterministic_shuffle(&mut b, &sk, round_id, 2, van);
1593 assert_ne!(
1594 a, b,
1595 "different proposals should produce different permutations"
1596 );
1597 }
1598
1599 #[test]
1600 fn shuffle_differs_across_vans() {
1601 let sk = test_sk();
1602 let round_id = test_round_id();
1603 let van_a = pallas::Base::from(0xAAAA_u64);
1604 let van_b = pallas::Base::from(0xBBBB_u64);
1605 let mut a = denomination_split(4_800, &sk, round_id, 1, van_a);
1606 let mut b = denomination_split(4_800, &sk, round_id, 1, van_b);
1607 deterministic_shuffle(&mut a, &sk, round_id, 1, van_a);
1608 deterministic_shuffle(&mut b, &sk, round_id, 1, van_b);
1609 assert_ne!(a, b, "different VANs should produce different permutations");
1610 }
1611
1612 #[test]
1613 fn shuffle_actually_reorders() {
1614 let sk = test_sk();
1615 let round_id = test_round_id();
1616 let van = test_van();
1617 let original = denomination_split(4_800, &sk, round_id, 1, van);
1618 let mut shuffled = original;
1619 deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
1620 assert_ne!(
1621 original, shuffled,
1622 "shuffle should reorder (vanishingly unlikely to be identity for 12 non-zero shares)"
1623 );
1624 }
1625
1626 #[test]
1627 fn prove_error_maps_into_build_error() {
1628 let err =
1629 VoteProofBuildError::from(ProveError::Halo2(halo2_proofs::plonk::Error::Synthesis));
1630
1631 assert!(matches!(err, VoteProofBuildError::Prove(_)));
1632 }
1633}