1use std::string::String;
13use std::vec::Vec;
14
15use ff::{FromUniformBytes, PrimeField};
16use group::{Curve, GroupEncoding};
17use halo2_proofs::circuit::Value;
18use pasta_curves::{arithmetic::CurveAffine, pallas};
19
20use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey};
21
22use super::circuit::{
23 share_commitment, shares_hash, van_integrity_hash, van_nullifier_hash, vote_commitment_hash,
24 Circuit, Instance, VOTE_COMM_TREE_DEPTH,
25};
26use super::prove::create_vote_proof;
27use super::{base_to_scalar, spend_auth_g_affine};
28
29const BALLOT_DIVISOR: u64 = 12_500_000;
31
32const NUM_SHARES: usize = 16;
34
35const DENOMINATIONS: [u64; 8] = [10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
48
49const MAX_DENOM_SHARES: usize = 9;
57
58const _: () = assert!(
61 NUM_SHARES - MAX_DENOM_SHARES >= 7,
62 "need at least 7 remainder slots for PRF-weighted distribution"
63);
64
65pub fn denomination_split(
77 num_ballots: u64,
78 sk: &SpendingKey,
79 round_id: pallas::Base,
80 proposal_id: u64,
81 van_commitment: pallas::Base,
82) -> [u64; NUM_SHARES] {
83 let mut shares = [0u64; NUM_SHARES];
84 let mut remaining = num_ballots;
85 let mut idx = 0;
86
87 for &d in &DENOMINATIONS {
91 while remaining >= d && idx < MAX_DENOM_SHARES {
92 shares[idx] = d;
93 remaining -= d;
94 idx += 1;
95 }
96 }
97
98 if remaining > 0 {
103 distribute_remainder(
104 &mut shares[idx..],
105 remaining,
106 sk,
107 round_id,
108 proposal_id,
109 van_commitment,
110 idx as u8,
111 );
112 }
113
114 shares
115}
116
117fn distribute_remainder(
123 slots: &mut [u64],
124 remainder: u64,
125 sk: &SpendingKey,
126 round_id: pallas::Base,
127 proposal_id: u64,
128 van_commitment: pallas::Base,
129 base_index: u8,
130) {
131 let n = slots.len() as u64;
132 if remainder < n {
140 for i in 0..(remainder as usize) {
141 slots[i] = 1;
142 }
143 return;
144 }
145
146 let distributable = remainder - n;
152
153 let mut weights = Vec::with_capacity(slots.len());
157 let mut total_weight: u64 = 0;
158 for i in 0..slots.len() {
159 let hash = vote_share_prf(
160 sk,
161 DOMAIN_REMAINDER,
162 round_id,
163 proposal_id,
164 van_commitment,
165 base_index.wrapping_add(i as u8),
166 );
167 let w = u32::from_le_bytes(hash[0..4].try_into().unwrap()) as u64 | 1;
168 weights.push(w);
169 total_weight += w;
170 }
171
172 let mut assigned: u64 = 0;
176 for i in 0..slots.len() {
177 let share = ((distributable as u128 * weights[i] as u128) / total_weight as u128) as u64;
178 slots[i] = 1 + share;
179 assigned += share;
180 }
181
182 let leftover = distributable - assigned;
186 for i in 0..(leftover as usize) {
187 slots[i] += 1;
188 }
189}
190
191#[derive(Debug, Clone)]
197pub struct EncryptedShareOutput {
198 pub c1: [u8; 32],
200 pub c2: [u8; 32],
202 pub share_index: u32,
204 pub plaintext_value: u64,
206 pub randomness: [u8; 32],
209}
210
211#[derive(Debug)]
213pub struct VoteProofBundle {
214 pub proof: Vec<u8>,
216 pub instance: Instance,
218 pub r_vpk_bytes: [u8; 32],
220 pub encrypted_shares: [EncryptedShareOutput; 16],
224 pub shares_hash: pallas::Base,
228 pub share_blinds: [pallas::Base; 16],
232 pub share_comms: [pallas::Base; 16],
237}
238
239#[derive(Debug)]
241pub enum VoteProofBuildError {
242 InvalidRandomness(String),
244 InvalidShares(String),
246}
247
248impl core::fmt::Display for VoteProofBuildError {
249 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
250 match self {
251 VoteProofBuildError::InvalidRandomness(msg) => {
252 write!(f, "invalid randomness: {}", msg)
253 }
254 VoteProofBuildError::InvalidShares(msg) => {
255 write!(f, "invalid shares: {}", msg)
256 }
257 }
258 }
259}
260
261fn extract_vsk(sk: &SpendingKey) -> pallas::Scalar {
266 let ask_raw = SpendAuthorizingKey::derive_inner(sk);
267 let g = pallas::Point::from(spend_auth_g_affine());
268 let ak_point = (g * ask_raw).to_affine();
269 let ak_bytes = ak_point.to_bytes();
270
271 if (ak_bytes.as_ref()[31] >> 7) == 1 {
273 -ask_raw
274 } else {
275 ask_raw
276 }
277}
278
279const VOTE_PRF_PERSONALIZATION: &[u8; 16] = b"ZcashVote_Expand";
282
283const DOMAIN_ELGAMAL: u8 = 0x00;
285const DOMAIN_BLIND: u8 = 0x01;
287const DOMAIN_SHUFFLE: u8 = 0x02;
289const DOMAIN_REMAINDER: u8 = 0x03;
291
292fn vote_share_prf(
303 sk: &SpendingKey,
304 domain: u8,
305 round_id: pallas::Base,
306 proposal_id: u64,
307 van_commitment: pallas::Base,
308 share_index: u8,
309) -> [u8; 64] {
310 *blake2b_simd::Params::new()
311 .hash_length(64)
312 .personal(VOTE_PRF_PERSONALIZATION)
313 .to_state()
314 .update(sk.to_bytes())
315 .update(&[domain])
316 .update(&round_id.to_repr())
317 .update(&proposal_id.to_le_bytes())
318 .update(&van_commitment.to_repr())
319 .update(&[share_index])
320 .finalize()
321 .as_array()
322}
323
324pub fn derive_share_randomness(
330 sk: &SpendingKey,
331 round_id: pallas::Base,
332 proposal_id: u64,
333 van_commitment: pallas::Base,
334 share_index: u8,
335) -> pallas::Base {
336 let hash = vote_share_prf(
337 sk,
338 DOMAIN_ELGAMAL,
339 round_id,
340 proposal_id,
341 van_commitment,
342 share_index,
343 );
344 let r = pallas::Base::from_uniform_bytes(&hash);
345 debug_assert!(base_to_scalar(r).is_some(), "p < q guarantees Base→Scalar");
346 r
347}
348
349pub fn derive_share_blind(
351 sk: &SpendingKey,
352 round_id: pallas::Base,
353 proposal_id: u64,
354 van_commitment: pallas::Base,
355 share_index: u8,
356) -> pallas::Base {
357 let hash = vote_share_prf(
358 sk,
359 DOMAIN_BLIND,
360 round_id,
361 proposal_id,
362 van_commitment,
363 share_index,
364 );
365 pallas::Base::from_uniform_bytes(&hash)
366}
367
368fn deterministic_shuffle(
382 shares: &mut [u64; NUM_SHARES],
383 sk: &SpendingKey,
384 round_id: pallas::Base,
385 proposal_id: u64,
386 van_commitment: pallas::Base,
387) {
388 let seed = vote_share_prf(sk, DOMAIN_SHUFFLE, round_id, proposal_id, van_commitment, 0);
392 for i in (1..NUM_SHARES).rev() {
393 let byte_offset = (NUM_SHARES - 1 - i) * 4;
397 let rand_bytes: [u8; 4] = seed[byte_offset..byte_offset + 4]
398 .try_into()
399 .expect("64-byte seed has room for 15 × 4-byte draws");
400 let j = (u32::from_le_bytes(rand_bytes) as usize) % (i + 1);
401 shares.swap(i, j);
402 }
403}
404
405#[allow(clippy::too_many_arguments)]
440pub fn build_vote_proof_from_delegation(
441 sk: &SpendingKey,
442 address_index: u32,
443 total_note_value: u64,
444 van_comm_rand: pallas::Base,
445 voting_round_id: pallas::Base,
446 vote_comm_tree_path: [pallas::Base; VOTE_COMM_TREE_DEPTH],
447 vote_comm_tree_position: u32,
448 anchor_height: u32,
449 proposal_id: u64,
450 vote_decision: u64,
451 ea_pk: pallas::Affine,
452 alpha_v: pallas::Scalar,
453 proposal_authority_old_u64: u64,
454 single_share: bool,
455) -> Result<VoteProofBundle, VoteProofBuildError> {
456 let vsk = extract_vsk(sk);
459 let fvk: FullViewingKey = sk.into();
460 let vsk_nk = fvk.nk().inner();
461 let rivk_v = fvk.rivk(Scope::External).inner();
462
463 let address = fvk.address_at(address_index, Scope::External);
464 let vpk_g_d_affine = address.g_d().to_affine();
465 let vpk_pk_d_affine = address.pk_d().inner().to_affine();
466
467 let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
468 let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
469
470 {
472 use core::iter;
473 use group::ff::PrimeFieldBits;
474 use halo2_gadgets::sinsemilla::primitives::CommitDomain;
475 use orchard::constants::{fixed_bases::COMMIT_IVK_PERSONALIZATION, L_ORCHARD_BASE};
476
477 let ak_from_vsk = (pallas::Point::from(spend_auth_g_affine()) * vsk).to_affine();
479 let fvk_bytes = fvk.to_bytes();
480 let ak_from_fvk_bytes: [u8; 32] = fvk_bytes[0..32].try_into().unwrap();
481 let ak_from_fvk: pallas::Affine = {
482 let opt: Option<pallas::Point> = pallas::Point::from_bytes(&ak_from_fvk_bytes).into();
483 opt.expect("ak from fvk must be a valid point").to_affine()
484 };
485 assert_eq!(
486 ak_from_vsk, ak_from_fvk,
487 "extract_vsk bug: [vsk]*SpendAuthG != ak from FullViewingKey"
488 );
489
490 let ak_x = *ak_from_vsk.coordinates().unwrap().x();
492 let domain = CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
493 let ivk = domain
494 .short_commit(
495 iter::empty()
496 .chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
497 .chain(vsk_nk.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
498 &rivk_v,
499 )
500 .expect("CommitIvk must not produce bottom");
501 let ivk_scalar = base_to_scalar(ivk).expect("ivk must be convertible to scalar");
502 let pk_d_derived = (pallas::Point::from(vpk_g_d_affine) * ivk_scalar).to_affine();
503 assert_eq!(
504 pk_d_derived, vpk_pk_d_affine,
505 "CommitIvk chain mismatch: [ivk]*g_d != pk_d from address"
506 );
507
508 std::eprintln!("[BUILDER] key-chain consistency checks passed");
509 }
510
511 let proposal_authority_old = pallas::Base::from(proposal_authority_old_u64);
514 let one_shifted = pallas::Base::from(1u64 << proposal_id);
515 let proposal_authority_new = proposal_authority_old - one_shifted;
516
517 let num_ballots = total_note_value / BALLOT_DIVISOR;
520 let num_ballots_base = pallas::Base::from(num_ballots);
521
522 let vote_authority_note_old = van_integrity_hash(
527 vpk_g_d_x,
528 vpk_pk_d_x,
529 num_ballots_base,
530 voting_round_id,
531 proposal_authority_old,
532 van_comm_rand,
533 );
534
535 let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
536
537 let vote_authority_note_new = van_integrity_hash(
538 vpk_g_d_x,
539 vpk_pk_d_x,
540 num_ballots_base,
541 voting_round_id,
542 proposal_authority_new,
543 van_comm_rand,
544 );
545
546 let shares_u64: [u64; 16] = if single_share {
551 let mut s = [0u64; 16];
555 s[0] = num_ballots;
556 s
557 } else {
558 let mut s = denomination_split(
559 num_ballots,
560 sk,
561 voting_round_id,
562 proposal_id,
563 vote_authority_note_old,
564 );
565 deterministic_shuffle(
566 &mut s,
567 sk,
568 voting_round_id,
569 proposal_id,
570 vote_authority_note_old,
571 );
572 s
573 };
574
575 for (i, &s) in shares_u64.iter().enumerate() {
577 if s >= (1u64 << 30) {
578 return Err(VoteProofBuildError::InvalidShares(format!(
579 "share {} = {} exceeds 2^30",
580 i, s
581 )));
582 }
583 }
584
585 let shares_base: [pallas::Base; 16] =
586 core::array::from_fn(|i| pallas::Base::from(shares_u64[i]));
587
588 let ea_pk_point = pallas::Point::from(ea_pk);
594 let ea_pk_x = *ea_pk.coordinates().unwrap().x();
595 let ea_pk_y = *ea_pk.coordinates().unwrap().y();
596
597 let g = pallas::Point::from(spend_auth_g_affine());
598 let mut enc_c1_x = [pallas::Base::zero(); 16];
599 let mut enc_c2_x = [pallas::Base::zero(); 16];
600 let mut enc_c1_y = [pallas::Base::zero(); 16];
601 let mut enc_c2_y = [pallas::Base::zero(); 16];
602 let mut share_randomness = [pallas::Base::zero(); 16];
603 let mut enc_share_outputs: [EncryptedShareOutput; 16] =
604 core::array::from_fn(|i| EncryptedShareOutput {
605 c1: [0u8; 32],
606 c2: [0u8; 32],
607 share_index: i as u32,
608 plaintext_value: shares_u64[i],
609 randomness: [0u8; 32],
610 });
611
612 for i in 0..16 {
613 let r = derive_share_randomness(
614 sk,
615 voting_round_id,
616 proposal_id,
617 vote_authority_note_old,
618 i as u8,
619 );
620 share_randomness[i] = r;
621 let r_scalar = base_to_scalar(r).expect("derive_share_randomness guarantees scalar-range");
622 let v_scalar = base_to_scalar(shares_base[i]).expect("share value in range");
623
624 let c1_point = (g * r_scalar).to_affine();
625 let c2_point = (g * v_scalar + ea_pk_point * r_scalar).to_affine();
626
627 enc_c1_x[i] = *c1_point.coordinates().unwrap().x();
628 enc_c2_x[i] = *c2_point.coordinates().unwrap().x();
629 enc_c1_y[i] = *c1_point.coordinates().unwrap().y();
630 enc_c2_y[i] = *c2_point.coordinates().unwrap().y();
631
632 enc_share_outputs[i].c1 = c1_point.to_bytes();
633 enc_share_outputs[i].c2 = c2_point.to_bytes();
634 enc_share_outputs[i].randomness = r.to_repr();
635 }
636
637 let share_blinds: [pallas::Base; 16] = core::array::from_fn(|i| {
638 derive_share_blind(
639 sk,
640 voting_round_id,
641 proposal_id,
642 vote_authority_note_old,
643 i as u8,
644 )
645 });
646 let share_comms: [pallas::Base; 16] = core::array::from_fn(|i| {
647 share_commitment(
648 share_blinds[i],
649 enc_c1_x[i],
650 enc_c2_x[i],
651 enc_c1_y[i],
652 enc_c2_y[i],
653 )
654 });
655 let shares_hash_val = shares_hash(share_blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
656
657 let ak_point = pallas::Point::from(spend_auth_g_affine()) * vsk;
660 let r_vpk = (ak_point + pallas::Point::from(spend_auth_g_affine()) * alpha_v).to_affine();
661 let r_vpk_x = *r_vpk.coordinates().unwrap().x();
662 let r_vpk_y = *r_vpk.coordinates().unwrap().y();
663 let r_vpk_bytes: [u8; 32] = r_vpk.to_bytes();
664
665 let proposal_id_base = pallas::Base::from(proposal_id);
668 let vote_decision_base = pallas::Base::from(vote_decision);
669 let vote_commitment = vote_commitment_hash(
670 voting_round_id,
671 shares_hash_val,
672 proposal_id_base,
673 vote_decision_base,
674 );
675
676 let vote_comm_tree_root = {
680 use super::circuit::poseidon_hash_2;
681
682 let mut current = vote_authority_note_old;
683 for level in 0..VOTE_COMM_TREE_DEPTH {
684 let sibling = vote_comm_tree_path[level];
685 if vote_comm_tree_position & (1 << level) == 0 {
686 current = poseidon_hash_2(current, sibling);
687 } else {
688 current = poseidon_hash_2(sibling, current);
689 }
690 }
691 current
692 };
693
694 let mut circuit = Circuit::with_van_witnesses(
697 Value::known(vote_comm_tree_path),
698 Value::known(vote_comm_tree_position),
699 Value::known(vpk_g_d_affine),
700 Value::known(vpk_pk_d_affine),
701 Value::known(num_ballots_base),
702 Value::known(proposal_authority_old),
703 Value::known(van_comm_rand),
704 Value::known(vote_authority_note_old),
705 Value::known(vsk),
706 Value::known(rivk_v),
707 Value::known(vsk_nk),
708 Value::known(alpha_v),
709 );
710 circuit.one_shifted = Value::known(one_shifted);
711 circuit.shares = shares_base.map(Value::known);
712 circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
713 circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
714 circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
715 circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
716 circuit.share_blinds = share_blinds.map(Value::known);
717 circuit.share_randomness = share_randomness.map(Value::known);
718 circuit.ea_pk = Value::known(ea_pk);
719 circuit.vote_decision = Value::known(vote_decision_base);
720
721 let anchor_height_base = pallas::Base::from(u64::from(anchor_height));
724 let instance = Instance::from_parts(
725 van_nullifier,
726 r_vpk_x,
727 r_vpk_y,
728 vote_authority_note_new,
729 vote_commitment,
730 vote_comm_tree_root,
731 anchor_height_base,
732 proposal_id_base,
733 voting_round_id,
734 ea_pk_x,
735 ea_pk_y,
736 );
737
738 {
741 use halo2_proofs::dev::MockProver;
742 let mock_circuit = circuit.clone();
743 let prover = MockProver::run(
744 super::circuit::K,
745 &mock_circuit,
746 vec![instance.to_halo2_instance()],
747 )
748 .expect("MockProver::run should not fail");
749
750 if let Err(failures) = prover.verify() {
751 return Err(VoteProofBuildError::InvalidShares(format!(
752 "circuit constraints not satisfied: {} failure(s): {:?}",
753 failures.len(),
754 failures,
755 )));
756 }
757 std::eprintln!("[BUILDER] MockProver passed");
758 }
759
760 let proof = create_vote_proof(circuit, &instance);
763
764 Ok(VoteProofBundle {
765 proof,
766 instance,
767 r_vpk_bytes,
768 encrypted_shares: enc_share_outputs,
769 shares_hash: shares_hash_val,
770 share_blinds,
771 share_comms,
772 })
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 fn test_sk() -> SpendingKey {
780 SpendingKey::from_bytes([0x42; 32]).expect("valid spending key")
781 }
782
783 fn test_round_id() -> pallas::Base {
784 pallas::Base::from(0xCAFE_u64)
785 }
786
787 fn test_van() -> pallas::Base {
788 pallas::Base::from(0xDEAD_u64)
789 }
790
791 #[test]
792 fn derive_share_randomness_is_deterministic() {
793 let sk = test_sk();
794 let round_id = test_round_id();
795 let van = test_van();
796 let a = derive_share_randomness(&sk, round_id, 1, van, 0);
797 let b = derive_share_randomness(&sk, round_id, 1, van, 0);
798 assert_eq!(a, b);
799 }
800
801 #[test]
802 fn derive_share_blind_is_deterministic() {
803 let sk = test_sk();
804 let round_id = test_round_id();
805 let van = test_van();
806 let a = derive_share_blind(&sk, round_id, 1, van, 0);
807 let b = derive_share_blind(&sk, round_id, 1, van, 0);
808 assert_eq!(a, b);
809 }
810
811 #[test]
812 fn derive_share_randomness_is_valid_scalar() {
813 let sk = test_sk();
814 let round_id = test_round_id();
815 let van = test_van();
816 for i in 0..16u8 {
817 let r = derive_share_randomness(&sk, round_id, 1, van, i);
818 assert!(
819 base_to_scalar(r).is_some(),
820 "r_{} must be convertible to scalar",
821 i
822 );
823 }
824 }
825
826 #[test]
827 fn different_share_index_gives_different_values() {
828 let sk = test_sk();
829 let round_id = test_round_id();
830 let van = test_van();
831 let r0 = derive_share_randomness(&sk, round_id, 1, van, 0);
832 let r1 = derive_share_randomness(&sk, round_id, 1, van, 1);
833 assert_ne!(r0, r1);
834
835 let b0 = derive_share_blind(&sk, round_id, 1, van, 0);
836 let b1 = derive_share_blind(&sk, round_id, 1, van, 1);
837 assert_ne!(b0, b1);
838 }
839
840 #[test]
841 fn different_proposal_id_gives_different_values() {
842 let sk = test_sk();
843 let round_id = test_round_id();
844 let van = test_van();
845 let r_p1 = derive_share_randomness(&sk, round_id, 1, van, 0);
846 let r_p2 = derive_share_randomness(&sk, round_id, 2, van, 0);
847 assert_ne!(r_p1, r_p2);
848 }
849
850 #[test]
851 fn different_round_id_gives_different_values() {
852 let sk = test_sk();
853 let van = test_van();
854 let r_a = derive_share_randomness(&sk, pallas::Base::from(1u64), 1, van, 0);
855 let r_b = derive_share_randomness(&sk, pallas::Base::from(2u64), 1, van, 0);
856 assert_ne!(r_a, r_b);
857 }
858
859 #[test]
860 fn randomness_and_blind_differ_for_same_inputs() {
861 let sk = test_sk();
862 let round_id = test_round_id();
863 let van = test_van();
864 let r = derive_share_randomness(&sk, round_id, 1, van, 0);
865 let b = derive_share_blind(&sk, round_id, 1, van, 0);
866 assert_ne!(r, b, "domain separation must prevent r == blind");
867 }
868
869 #[test]
870 fn all_16_shares_are_distinct() {
871 let sk = test_sk();
872 let round_id = test_round_id();
873 let van = test_van();
874 let randoms: Vec<_> = (0..16u8)
875 .map(|i| derive_share_randomness(&sk, round_id, 1, van, i))
876 .collect();
877 let blinds: Vec<_> = (0..16u8)
878 .map(|i| derive_share_blind(&sk, round_id, 1, van, i))
879 .collect();
880 for i in 0..16 {
881 for j in (i + 1)..16 {
882 assert_ne!(randoms[i], randoms[j], "r_{} == r_{}", i, j);
883 assert_ne!(blinds[i], blinds[j], "blind_{} == blind_{}", i, j);
884 }
885 }
886 }
887
888 #[test]
889 fn different_van_commitment_gives_different_values() {
890 let sk = test_sk();
891 let round_id = test_round_id();
892 let van_a = pallas::Base::from(0xAAAA_u64);
893 let van_b = pallas::Base::from(0xBBBB_u64);
894 for i in 0..16u8 {
895 let r_a = derive_share_randomness(&sk, round_id, 1, van_a, i);
896 let r_b = derive_share_randomness(&sk, round_id, 1, van_b, i);
897 assert_ne!(r_a, r_b, "r_{} must differ across VANs", i);
898
899 let b_a = derive_share_blind(&sk, round_id, 1, van_a, i);
900 let b_b = derive_share_blind(&sk, round_id, 1, van_b, i);
901 assert_ne!(b_a, b_b, "blind_{} must differ across VANs", i);
902 }
903 }
904
905 fn show(label: &str, shares: &[u64; 16]) {
917 let parts: Vec<String> = shares
918 .iter()
919 .map(|&v| {
920 if v == 0 {
921 "0".into()
922 } else if v >= 1_000_000 {
923 format!("{}M", v / 1_000_000)
924 } else if v >= 1_000 {
925 format!("{}K", v / 1_000)
926 } else {
927 format!("{}", v)
928 }
929 })
930 .collect();
931 std::eprintln!(" {}: [{}]", label, parts.join(", "));
932 }
933
934 #[test]
935 fn denom_split_zero_ballots() {
936 let sk = test_sk();
939 let rid = test_round_id();
940 let van = test_van();
941 let shares = denomination_split(0, &sk, rid, 1, van);
942 show("0 ballots", &shares);
943 assert_eq!(shares, [0; 16]);
944 }
945
946 #[test]
947 fn denom_split_single_ballot() {
948 let sk = test_sk();
951 let rid = test_round_id();
952 let van = test_van();
953 let shares = denomination_split(1, &sk, rid, 1, van);
954 show("1 ballot (0.125 ZEC)", &shares);
955 assert_eq!(shares[0], 1);
956 for i in 1..16 {
957 assert_eq!(shares[i], 0);
958 }
959 }
960
961 #[test]
962 fn denom_split_sub_zec() {
963 let sk = test_sk();
966 let rid = test_round_id();
967 let van = test_van();
968 let shares = denomination_split(4, &sk, rid, 1, van);
969 show("4 ballots (0.5 ZEC)", &shares);
970 assert_eq!(shares[0..4], [1; 4]);
971 for i in 4..16 {
972 assert_eq!(shares[i], 0);
973 }
974 }
975
976 #[test]
977 fn denom_split_one_zec() {
978 let sk = test_sk();
981 let rid = test_round_id();
982 let van = test_van();
983 let shares = denomination_split(8, &sk, rid, 1, van);
984 show("8 ballots (1 ZEC)", &shares);
985 assert_eq!(shares[0..8], [1; 8]);
986 for i in 8..16 {
987 assert_eq!(shares[i], 0);
988 }
989 }
990
991 #[test]
992 fn denom_split_small_balance() {
993 let sk = test_sk();
996 let rid = test_round_id();
997 let van = test_van();
998 let shares = denomination_split(50, &sk, rid, 1, van);
999 show("50 ballots (6.25 ZEC)", &shares);
1000 assert_eq!(shares[0..5], [10; 5]);
1001 for i in 5..16 {
1002 assert_eq!(shares[i], 0);
1003 }
1004 }
1005
1006 #[test]
1007 fn denom_split_all_denoms_exact() {
1008 let sk = test_sk();
1011 let rid = test_round_id();
1012 let van = test_van();
1013 let shares = denomination_split(11_111, &sk, rid, 1, van);
1014 show("11,111 ballots (1,388.9 ZEC)", &shares);
1015 assert_eq!(shares[0], 10_000);
1016 assert_eq!(shares[1], 1_000);
1017 assert_eq!(shares[2], 100);
1018 assert_eq!(shares[3], 10);
1019 assert_eq!(shares[4], 1);
1020 for i in 5..16 {
1021 assert_eq!(shares[i], 0);
1022 }
1023 }
1024
1025 #[test]
1026 fn denom_split_medium_holder_with_remainder() {
1027 let sk = test_sk();
1032 let rid = test_round_id();
1033 let van = test_van();
1034 let shares = denomination_split(4_800, &sk, rid, 1, van);
1035 show("4,800 ballots (600 ZEC)", &shares);
1036 assert_eq!(shares[0..4], [1_000; 4]);
1037 assert_eq!(shares[4..9], [100; 5]);
1038 let remainder_sum: u64 = shares[9..16].iter().sum();
1039 assert_eq!(remainder_sum, 300);
1040 for i in 9..16 {
1041 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1042 }
1043 assert_eq!(shares.iter().sum::<u64>(), 4_800);
1044 }
1045
1046 #[test]
1047 fn denom_split_high_hamming_weight() {
1048 let sk = test_sk();
1053 let rid = test_round_id();
1054 let van = test_van();
1055 let shares = denomination_split(999, &sk, rid, 1, van);
1056 show("999 ballots (124.875 ZEC)", &shares);
1057 assert_eq!(shares[0..9], [100; 9]);
1058 let remainder_sum: u64 = shares[9..16].iter().sum();
1059 assert_eq!(remainder_sum, 99);
1060 for i in 9..16 {
1061 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1062 }
1063 }
1064
1065 #[test]
1066 fn denom_split_exact_denomination_match() {
1067 let sk = test_sk();
1070 let rid = test_round_id();
1071 let van = test_van();
1072 let shares = denomination_split(3_000_000, &sk, rid, 1, van);
1073 show("3M ballots (375 ZEC)", &shares);
1074 assert_eq!(shares[0..3], [1_000_000; 3]);
1075 for i in 3..16 {
1076 assert_eq!(shares[i], 0);
1077 }
1078 }
1079
1080 #[test]
1081 fn denom_split_8m_ballots() {
1082 let sk = test_sk();
1085 let rid = test_round_id();
1086 let van = test_van();
1087 let shares = denomination_split(8_000_000, &sk, rid, 1, van);
1088 show("8M ballots (1M ZEC)", &shares);
1089 assert_eq!(shares[0..8], [1_000_000; 8]);
1090 for i in 8..16 {
1091 assert_eq!(shares[i], 0);
1092 }
1093 }
1094
1095 #[test]
1096 fn denom_split_fills_all_9_denom_slots() {
1097 let sk = test_sk();
1100 let rid = test_round_id();
1101 let van = test_van();
1102 let shares = denomination_split(90_000_000, &sk, rid, 1, van);
1103 show("90M ballots (11.25M ZEC)", &shares);
1104 assert_eq!(shares[0..9], [10_000_000; 9]);
1105 for i in 9..16 {
1106 assert_eq!(shares[i], 0);
1107 }
1108 }
1109
1110 #[test]
1111 fn denom_split_overflow_into_remainder() {
1112 let sk = test_sk();
1117 let rid = test_round_id();
1118 let van = test_van();
1119 let shares = denomination_split(100_000_000, &sk, rid, 1, van);
1120 show("100M ballots (12.5M ZEC)", &shares);
1121 assert_eq!(shares[0..9], [10_000_000; 9]);
1122 let remainder_sum: u64 = shares[9..16].iter().sum();
1123 assert_eq!(remainder_sum, 10_000_000);
1124 for i in 9..16 {
1125 assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
1126 }
1127 }
1128
1129 #[test]
1130 fn denom_split_mixed_with_remainder() {
1131 let sk = test_sk();
1135 let rid = test_round_id();
1136 let van = test_van();
1137 let shares = denomination_split(1_234_567, &sk, rid, 1, van);
1138 show("1,234,567 ballots (154K ZEC)", &shares);
1139 assert_eq!(shares[0], 1_000_000);
1140 assert_eq!(shares[1..3], [100_000; 2]);
1141 assert_eq!(shares[3..6], [10_000; 3]);
1142 assert_eq!(shares[6..9], [1_000; 3]);
1143 let remainder_sum: u64 = shares[9..16].iter().sum();
1144 assert_eq!(remainder_sum, 1_567);
1145 assert_eq!(shares.iter().sum::<u64>(), 1_234_567);
1146 }
1147
1148 #[test]
1149 fn denom_split_small_remainder_fewer_than_free_slots() {
1150 let sk = test_sk();
1154 let rid = test_round_id();
1155 let van = test_van();
1156 let shares = denomination_split(10_000_003, &sk, rid, 1, van);
1157 show("10,000,003 ballots", &shares);
1158 assert_eq!(shares[0], 10_000_000);
1159 let remainder_sum: u64 = shares[1..16].iter().sum();
1160 assert_eq!(remainder_sum, 3);
1161 assert_eq!(shares.iter().sum::<u64>(), 10_000_003);
1162 }
1163
1164 #[test]
1167 fn denom_split_sum_invariant() {
1168 let sk = test_sk();
1169 let rid = test_round_id();
1170 let van = test_van();
1171 let test_values: [u64; 14] = [
1172 0,
1173 1,
1174 50,
1175 99,
1176 100,
1177 999,
1178 1_000,
1179 10_000,
1180 100_000,
1181 1_000_000,
1182 8_234_567,
1183 20_000_000,
1184 80_000_000,
1185 168_000_000,
1186 ];
1187 for &v in &test_values {
1188 let shares = denomination_split(v, &sk, rid, 1, van);
1189 assert_eq!(
1190 shares.iter().sum::<u64>(),
1191 v,
1192 "sum invariant violated for {}",
1193 v
1194 );
1195 }
1196 }
1197
1198 #[test]
1199 fn denom_split_all_shares_in_range() {
1200 let sk = test_sk();
1201 let rid = test_round_id();
1202 let van = test_van();
1203 let test_values: [u64; 8] = [
1204 1,
1205 10_000,
1206 1_000_000,
1207 8_234_567,
1208 15_000_000,
1209 20_000_000,
1210 80_000_000,
1211 168_000_000,
1212 ];
1213 for &v in &test_values {
1214 let shares = denomination_split(v, &sk, rid, 1, van);
1215 for (i, &s) in shares.iter().enumerate() {
1216 assert!(
1217 s < (1u64 << 30),
1218 "share {} = {} exceeds 2^30 for {}",
1219 i,
1220 s,
1221 v
1222 );
1223 }
1224 }
1225 }
1226
1227 #[test]
1230 fn remainder_is_deterministic() {
1231 let sk = test_sk();
1232 let rid = test_round_id();
1233 let van = test_van();
1234 let a = denomination_split(999, &sk, rid, 1, van);
1235 let b = denomination_split(999, &sk, rid, 1, van);
1236 assert_eq!(a, b);
1237 }
1238
1239 #[test]
1240 fn remainder_differs_across_proposals() {
1241 let sk = test_sk();
1243 let rid = test_round_id();
1244 let van = test_van();
1245 let a = denomination_split(999, &sk, rid, 1, van);
1246 let b = denomination_split(999, &sk, rid, 2, van);
1247 show("999 ballots, proposal 1", &a);
1248 show("999 ballots, proposal 2", &b);
1249 assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
1250 assert_ne!(
1251 a[9..16],
1252 b[9..16],
1253 "remainder should differ across proposals"
1254 );
1255 }
1256
1257 #[test]
1258 fn remainder_differs_across_vans() {
1259 let sk = test_sk();
1261 let rid = test_round_id();
1262 let van_a = pallas::Base::from(0xAAAA_u64);
1263 let van_b = pallas::Base::from(0xBBBB_u64);
1264 let a = denomination_split(999, &sk, rid, 1, van_a);
1265 let b = denomination_split(999, &sk, rid, 1, van_b);
1266 show("999 ballots, VAN A", &a);
1267 show("999 ballots, VAN B", &b);
1268 assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
1269 assert_ne!(a[9..16], b[9..16], "remainder should differ across VANs");
1270 }
1271
1272 #[test]
1275 fn shuffle_preserves_sum() {
1276 let sk = test_sk();
1277 let round_id = test_round_id();
1278 let van = test_van();
1279 let mut shares = denomination_split(8_234_567, &sk, round_id, 1, van);
1280 let sum_before = shares.iter().sum::<u64>();
1281 deterministic_shuffle(&mut shares, &sk, round_id, 1, van);
1282 assert_eq!(shares.iter().sum::<u64>(), sum_before);
1283 }
1284
1285 #[test]
1286 fn shuffle_preserves_multiset() {
1287 let sk = test_sk();
1288 let round_id = test_round_id();
1289 let van = test_van();
1290 let original = denomination_split(4_800, &sk, round_id, 1, van);
1291 let mut shuffled = original;
1292 deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
1293 let mut sorted_orig = original;
1294 sorted_orig.sort();
1295 let mut sorted_shuf = shuffled;
1296 sorted_shuf.sort();
1297 assert_eq!(sorted_orig, sorted_shuf, "shuffle must be a permutation");
1298 }
1299
1300 #[test]
1301 fn shuffle_is_deterministic() {
1302 let sk = test_sk();
1303 let round_id = test_round_id();
1304 let van = test_van();
1305 let mut a = denomination_split(4_800, &sk, round_id, 1, van);
1306 let mut b = denomination_split(4_800, &sk, round_id, 1, van);
1307 deterministic_shuffle(&mut a, &sk, round_id, 1, van);
1308 deterministic_shuffle(&mut b, &sk, round_id, 1, van);
1309 assert_eq!(a, b, "same inputs must produce same permutation");
1310 }
1311
1312 #[test]
1313 fn shuffle_differs_across_proposals() {
1314 let sk = test_sk();
1315 let round_id = test_round_id();
1316 let van = test_van();
1317 let mut a = denomination_split(4_800, &sk, round_id, 1, van);
1318 let mut b = denomination_split(4_800, &sk, round_id, 1, van);
1319 deterministic_shuffle(&mut a, &sk, round_id, 1, van);
1320 deterministic_shuffle(&mut b, &sk, round_id, 2, van);
1321 assert_ne!(
1322 a, b,
1323 "different proposals should produce different permutations"
1324 );
1325 }
1326
1327 #[test]
1328 fn shuffle_differs_across_vans() {
1329 let sk = test_sk();
1330 let round_id = test_round_id();
1331 let van_a = pallas::Base::from(0xAAAA_u64);
1332 let van_b = pallas::Base::from(0xBBBB_u64);
1333 let mut a = denomination_split(4_800, &sk, round_id, 1, van_a);
1334 let mut b = denomination_split(4_800, &sk, round_id, 1, van_b);
1335 deterministic_shuffle(&mut a, &sk, round_id, 1, van_a);
1336 deterministic_shuffle(&mut b, &sk, round_id, 1, van_b);
1337 assert_ne!(a, b, "different VANs should produce different permutations");
1338 }
1339
1340 #[test]
1341 fn shuffle_actually_reorders() {
1342 let sk = test_sk();
1343 let round_id = test_round_id();
1344 let van = test_van();
1345 let original = denomination_split(4_800, &sk, round_id, 1, van);
1346 let mut shuffled = original;
1347 deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
1348 assert_ne!(
1349 original, shuffled,
1350 "shuffle should reorder (vanishingly unlikely to be identity for 12 non-zero shares)"
1351 );
1352 }
1353}