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