use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use ff::{FromUniformBytes, PrimeField};
use group::{Curve, GroupEncoding};
use halo2_proofs::circuit::Value;
use pasta_curves::{arithmetic::CurveAffine, pallas};
use orchard::keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey};
use super::circuit::{
share_commitment, shares_hash, van_integrity_hash, van_nullifier_hash, vote_commitment_hash,
Circuit, Instance, VOTE_COMM_TREE_DEPTH,
};
use super::prove::create_vote_proof;
use super::{base_to_scalar, spend_auth_g_affine};
const BALLOT_DIVISOR: u64 = 12_500_000;
const NUM_SHARES: usize = 16;
const DENOMINATIONS: [u64; 8] = [10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
const MAX_DENOM_SHARES: usize = 9;
const _: () = assert!(
NUM_SHARES - MAX_DENOM_SHARES >= 7,
"need at least 7 remainder slots for PRF-weighted distribution"
);
pub fn denomination_split(
num_ballots: u64,
sk: &SpendingKey,
round_id: pallas::Base,
proposal_id: u64,
van_commitment: pallas::Base,
) -> [u64; NUM_SHARES] {
let mut shares = [0u64; NUM_SHARES];
let mut remaining = num_ballots;
let mut idx = 0;
for &d in &DENOMINATIONS {
while remaining >= d && idx < MAX_DENOM_SHARES {
shares[idx] = d;
remaining -= d;
idx += 1;
}
}
if remaining > 0 {
distribute_remainder(
&mut shares[idx..],
remaining,
sk,
round_id,
proposal_id,
van_commitment,
idx as u8,
);
}
shares
}
fn distribute_remainder(
slots: &mut [u64],
remainder: u64,
sk: &SpendingKey,
round_id: pallas::Base,
proposal_id: u64,
van_commitment: pallas::Base,
base_index: u8,
) {
let n = slots.len() as u64;
if remainder < n {
for i in 0..(remainder as usize) {
slots[i] = 1;
}
return;
}
let distributable = remainder - n;
let mut weights = Vec::with_capacity(slots.len());
let mut total_weight: u64 = 0;
for i in 0..slots.len() {
let hash = vote_share_prf(
sk,
DOMAIN_REMAINDER,
round_id,
proposal_id,
van_commitment,
base_index.wrapping_add(i as u8),
);
let w = u32::from_le_bytes(hash[0..4].try_into().unwrap()) as u64 | 1;
weights.push(w);
total_weight += w;
}
let mut assigned: u64 = 0;
for i in 0..slots.len() {
let share = ((distributable as u128 * weights[i] as u128) / total_weight as u128) as u64;
slots[i] = 1 + share;
assigned += share;
}
let leftover = distributable - assigned;
for i in 0..(leftover as usize) {
slots[i] += 1;
}
}
#[derive(Debug, Clone)]
pub struct EncryptedShareOutput {
pub c1: [u8; 32],
pub c2: [u8; 32],
pub share_index: u32,
pub plaintext_value: u64,
pub randomness: [u8; 32],
}
#[derive(Debug)]
pub struct VoteProofBundle {
pub proof: Vec<u8>,
pub instance: Instance,
pub r_vpk_bytes: [u8; 32],
pub encrypted_shares: [EncryptedShareOutput; 16],
pub shares_hash: pallas::Base,
pub share_blinds: [pallas::Base; 16],
pub share_comms: [pallas::Base; 16],
}
#[derive(Debug)]
pub enum VoteProofBuildError {
InvalidRandomness(String),
InvalidShares(String),
}
impl core::fmt::Display for VoteProofBuildError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
VoteProofBuildError::InvalidRandomness(msg) => {
write!(f, "invalid randomness: {}", msg)
}
VoteProofBuildError::InvalidShares(msg) => {
write!(f, "invalid shares: {}", msg)
}
}
}
}
fn extract_vsk(sk: &SpendingKey) -> pallas::Scalar {
let ask_raw = SpendAuthorizingKey::derive_inner(sk);
let g = pallas::Point::from(spend_auth_g_affine());
let ak_point = (g * ask_raw).to_affine();
let ak_bytes = ak_point.to_bytes();
if (ak_bytes.as_ref()[31] >> 7) == 1 {
-ask_raw
} else {
ask_raw
}
}
const VOTE_PRF_PERSONALIZATION: &[u8; 16] = b"ZcashVote_Expand";
const DOMAIN_ELGAMAL: u8 = 0x00;
const DOMAIN_BLIND: u8 = 0x01;
const DOMAIN_SHUFFLE: u8 = 0x02;
const DOMAIN_REMAINDER: u8 = 0x03;
fn vote_share_prf(
sk: &SpendingKey,
domain: u8,
round_id: pallas::Base,
proposal_id: u64,
van_commitment: pallas::Base,
share_index: u8,
) -> [u8; 64] {
*blake2b_simd::Params::new()
.hash_length(64)
.personal(VOTE_PRF_PERSONALIZATION)
.to_state()
.update(sk.to_bytes())
.update(&[domain])
.update(&round_id.to_repr())
.update(&proposal_id.to_le_bytes())
.update(&van_commitment.to_repr())
.update(&[share_index])
.finalize()
.as_array()
}
pub fn derive_share_randomness(
sk: &SpendingKey,
round_id: pallas::Base,
proposal_id: u64,
van_commitment: pallas::Base,
share_index: u8,
) -> pallas::Base {
let hash = vote_share_prf(
sk,
DOMAIN_ELGAMAL,
round_id,
proposal_id,
van_commitment,
share_index,
);
let r = pallas::Base::from_uniform_bytes(&hash);
debug_assert!(base_to_scalar(r).is_some(), "p < q guarantees Base→Scalar");
r
}
pub fn derive_share_blind(
sk: &SpendingKey,
round_id: pallas::Base,
proposal_id: u64,
van_commitment: pallas::Base,
share_index: u8,
) -> pallas::Base {
let hash = vote_share_prf(
sk,
DOMAIN_BLIND,
round_id,
proposal_id,
van_commitment,
share_index,
);
pallas::Base::from_uniform_bytes(&hash)
}
fn deterministic_shuffle(
shares: &mut [u64; NUM_SHARES],
sk: &SpendingKey,
round_id: pallas::Base,
proposal_id: u64,
van_commitment: pallas::Base,
) {
let seed = vote_share_prf(sk, DOMAIN_SHUFFLE, round_id, proposal_id, van_commitment, 0);
for i in (1..NUM_SHARES).rev() {
let byte_offset = (NUM_SHARES - 1 - i) * 4;
let rand_bytes: [u8; 4] = seed[byte_offset..byte_offset + 4]
.try_into()
.expect("64-byte seed has room for 15 × 4-byte draws");
let j = (u32::from_le_bytes(rand_bytes) as usize) % (i + 1);
shares.swap(i, j);
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_vote_proof_from_delegation(
sk: &SpendingKey,
address_index: u32,
total_note_value: u64,
van_comm_rand: pallas::Base,
voting_round_id: pallas::Base,
vote_comm_tree_path: [pallas::Base; VOTE_COMM_TREE_DEPTH],
vote_comm_tree_position: u32,
anchor_height: u32,
proposal_id: u64,
vote_decision: u64,
ea_pk: pallas::Affine,
alpha_v: pallas::Scalar,
proposal_authority_old_u64: u64,
single_share: bool,
) -> Result<VoteProofBundle, VoteProofBuildError> {
let vsk = extract_vsk(sk);
let fvk: FullViewingKey = sk.into();
let vsk_nk = fvk.nk().inner();
let rivk_v = fvk.rivk(Scope::External).inner();
let address = fvk.address_at(address_index, Scope::External);
let vpk_g_d_affine = address.g_d().to_affine();
let vpk_pk_d_affine = address.pk_d().inner().to_affine();
let vpk_g_d_x = *vpk_g_d_affine.coordinates().unwrap().x();
let vpk_pk_d_x = *vpk_pk_d_affine.coordinates().unwrap().x();
{
use core::iter;
use group::ff::PrimeFieldBits;
use halo2_gadgets::sinsemilla::primitives::CommitDomain;
use orchard::constants::{fixed_bases::COMMIT_IVK_PERSONALIZATION, L_ORCHARD_BASE};
let ak_from_vsk = (pallas::Point::from(spend_auth_g_affine()) * vsk).to_affine();
let fvk_bytes = fvk.to_bytes();
let ak_from_fvk_bytes: [u8; 32] = fvk_bytes[0..32].try_into().unwrap();
let ak_from_fvk: pallas::Affine = {
let opt: Option<pallas::Point> = pallas::Point::from_bytes(&ak_from_fvk_bytes).into();
opt.expect("ak from fvk must be a valid point").to_affine()
};
assert_eq!(
ak_from_vsk, ak_from_fvk,
"extract_vsk bug: [vsk]*SpendAuthG != ak from FullViewingKey"
);
let ak_x = *ak_from_vsk.coordinates().unwrap().x();
let domain = CommitDomain::new(COMMIT_IVK_PERSONALIZATION);
let ivk = domain
.short_commit(
iter::empty()
.chain(ak_x.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE))
.chain(vsk_nk.to_le_bits().iter().by_vals().take(L_ORCHARD_BASE)),
&rivk_v,
)
.expect("CommitIvk must not produce bottom");
let ivk_scalar = base_to_scalar(ivk).expect("ivk must be convertible to scalar");
let pk_d_derived = (pallas::Point::from(vpk_g_d_affine) * ivk_scalar).to_affine();
assert_eq!(
pk_d_derived, vpk_pk_d_affine,
"CommitIvk chain mismatch: [ivk]*g_d != pk_d from address"
);
std::eprintln!("[BUILDER] key-chain consistency checks passed");
}
let proposal_authority_old = pallas::Base::from(proposal_authority_old_u64);
let one_shifted = pallas::Base::from(1u64 << proposal_id);
let proposal_authority_new = proposal_authority_old - one_shifted;
let num_ballots = total_note_value / BALLOT_DIVISOR;
let num_ballots_base = pallas::Base::from(num_ballots);
let vote_authority_note_old = van_integrity_hash(
vpk_g_d_x,
vpk_pk_d_x,
num_ballots_base,
voting_round_id,
proposal_authority_old,
van_comm_rand,
);
let van_nullifier = van_nullifier_hash(vsk_nk, voting_round_id, vote_authority_note_old);
let vote_authority_note_new = van_integrity_hash(
vpk_g_d_x,
vpk_pk_d_x,
num_ballots_base,
voting_round_id,
proposal_authority_new,
van_comm_rand,
);
let shares_u64: [u64; 16] = if single_share {
let mut s = [0u64; 16];
s[0] = num_ballots;
s
} else {
let mut s = denomination_split(
num_ballots,
sk,
voting_round_id,
proposal_id,
vote_authority_note_old,
);
deterministic_shuffle(
&mut s,
sk,
voting_round_id,
proposal_id,
vote_authority_note_old,
);
s
};
for (i, &s) in shares_u64.iter().enumerate() {
if s >= (1u64 << 30) {
return Err(VoteProofBuildError::InvalidShares(format!(
"share {} = {} exceeds 2^30",
i, s
)));
}
}
let shares_base: [pallas::Base; 16] =
core::array::from_fn(|i| pallas::Base::from(shares_u64[i]));
let ea_pk_point = pallas::Point::from(ea_pk);
let ea_pk_x = *ea_pk.coordinates().unwrap().x();
let ea_pk_y = *ea_pk.coordinates().unwrap().y();
let g = pallas::Point::from(spend_auth_g_affine());
let mut enc_c1_x = [pallas::Base::zero(); 16];
let mut enc_c2_x = [pallas::Base::zero(); 16];
let mut enc_c1_y = [pallas::Base::zero(); 16];
let mut enc_c2_y = [pallas::Base::zero(); 16];
let mut share_randomness = [pallas::Base::zero(); 16];
let mut enc_share_outputs: [EncryptedShareOutput; 16] =
core::array::from_fn(|i| EncryptedShareOutput {
c1: [0u8; 32],
c2: [0u8; 32],
share_index: i as u32,
plaintext_value: shares_u64[i],
randomness: [0u8; 32],
});
for i in 0..16 {
let r = derive_share_randomness(
sk,
voting_round_id,
proposal_id,
vote_authority_note_old,
i as u8,
);
share_randomness[i] = r;
let r_scalar = base_to_scalar(r).expect("derive_share_randomness guarantees scalar-range");
let v_scalar = base_to_scalar(shares_base[i]).expect("share value in range");
let c1_point = (g * r_scalar).to_affine();
let c2_point = (g * v_scalar + ea_pk_point * r_scalar).to_affine();
enc_c1_x[i] = *c1_point.coordinates().unwrap().x();
enc_c2_x[i] = *c2_point.coordinates().unwrap().x();
enc_c1_y[i] = *c1_point.coordinates().unwrap().y();
enc_c2_y[i] = *c2_point.coordinates().unwrap().y();
enc_share_outputs[i].c1 = c1_point.to_bytes();
enc_share_outputs[i].c2 = c2_point.to_bytes();
enc_share_outputs[i].randomness = r.to_repr();
}
let share_blinds: [pallas::Base; 16] = core::array::from_fn(|i| {
derive_share_blind(
sk,
voting_round_id,
proposal_id,
vote_authority_note_old,
i as u8,
)
});
let share_comms: [pallas::Base; 16] = core::array::from_fn(|i| {
share_commitment(
share_blinds[i],
enc_c1_x[i],
enc_c2_x[i],
enc_c1_y[i],
enc_c2_y[i],
)
});
let shares_hash_val = shares_hash(share_blinds, enc_c1_x, enc_c2_x, enc_c1_y, enc_c2_y);
let ak_point = pallas::Point::from(spend_auth_g_affine()) * vsk;
let r_vpk = (ak_point + pallas::Point::from(spend_auth_g_affine()) * alpha_v).to_affine();
let r_vpk_x = *r_vpk.coordinates().unwrap().x();
let r_vpk_y = *r_vpk.coordinates().unwrap().y();
let r_vpk_bytes: [u8; 32] = r_vpk.to_bytes();
let proposal_id_base = pallas::Base::from(proposal_id);
let vote_decision_base = pallas::Base::from(vote_decision);
let vote_commitment = vote_commitment_hash(
voting_round_id,
shares_hash_val,
proposal_id_base,
vote_decision_base,
);
let vote_comm_tree_root = {
use super::circuit::poseidon_hash_2;
let mut current = vote_authority_note_old;
for level in 0..VOTE_COMM_TREE_DEPTH {
let sibling = vote_comm_tree_path[level];
if vote_comm_tree_position & (1 << level) == 0 {
current = poseidon_hash_2(current, sibling);
} else {
current = poseidon_hash_2(sibling, current);
}
}
current
};
let mut circuit = Circuit::with_van_witnesses(
Value::known(vote_comm_tree_path),
Value::known(vote_comm_tree_position),
Value::known(vpk_g_d_affine),
Value::known(vpk_pk_d_affine),
Value::known(num_ballots_base),
Value::known(proposal_authority_old),
Value::known(van_comm_rand),
Value::known(vote_authority_note_old),
Value::known(vsk),
Value::known(rivk_v),
Value::known(vsk_nk),
Value::known(alpha_v),
);
circuit.one_shifted = Value::known(one_shifted);
circuit.shares = shares_base.map(Value::known);
circuit.enc_share_c1_x = enc_c1_x.map(Value::known);
circuit.enc_share_c2_x = enc_c2_x.map(Value::known);
circuit.enc_share_c1_y = enc_c1_y.map(Value::known);
circuit.enc_share_c2_y = enc_c2_y.map(Value::known);
circuit.share_blinds = share_blinds.map(Value::known);
circuit.share_randomness = share_randomness.map(Value::known);
circuit.ea_pk = Value::known(ea_pk);
circuit.vote_decision = Value::known(vote_decision_base);
let anchor_height_base = pallas::Base::from(u64::from(anchor_height));
let instance = Instance::from_parts(
van_nullifier,
r_vpk_x,
r_vpk_y,
vote_authority_note_new,
vote_commitment,
vote_comm_tree_root,
anchor_height_base,
proposal_id_base,
voting_round_id,
ea_pk_x,
ea_pk_y,
);
{
use halo2_proofs::dev::MockProver;
let mock_circuit = circuit.clone();
let prover = MockProver::run(
super::circuit::K,
&mock_circuit,
vec![instance.to_halo2_instance()],
)
.expect("MockProver::run should not fail");
if let Err(failures) = prover.verify() {
return Err(VoteProofBuildError::InvalidShares(format!(
"circuit constraints not satisfied: {} failure(s): {:?}",
failures.len(),
failures,
)));
}
std::eprintln!("[BUILDER] MockProver passed");
}
let proof = create_vote_proof(circuit, &instance);
Ok(VoteProofBundle {
proof,
instance,
r_vpk_bytes,
encrypted_shares: enc_share_outputs,
shares_hash: shares_hash_val,
share_blinds,
share_comms,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn test_sk() -> SpendingKey {
SpendingKey::from_bytes([0x42; 32]).expect("valid spending key")
}
fn test_round_id() -> pallas::Base {
pallas::Base::from(0xCAFE_u64)
}
fn test_van() -> pallas::Base {
pallas::Base::from(0xDEAD_u64)
}
#[test]
fn derive_share_randomness_is_deterministic() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let a = derive_share_randomness(&sk, round_id, 1, van, 0);
let b = derive_share_randomness(&sk, round_id, 1, van, 0);
assert_eq!(a, b);
}
#[test]
fn derive_share_blind_is_deterministic() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let a = derive_share_blind(&sk, round_id, 1, van, 0);
let b = derive_share_blind(&sk, round_id, 1, van, 0);
assert_eq!(a, b);
}
#[test]
fn derive_share_randomness_is_valid_scalar() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
for i in 0..16u8 {
let r = derive_share_randomness(&sk, round_id, 1, van, i);
assert!(
base_to_scalar(r).is_some(),
"r_{} must be convertible to scalar",
i
);
}
}
#[test]
fn different_share_index_gives_different_values() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let r0 = derive_share_randomness(&sk, round_id, 1, van, 0);
let r1 = derive_share_randomness(&sk, round_id, 1, van, 1);
assert_ne!(r0, r1);
let b0 = derive_share_blind(&sk, round_id, 1, van, 0);
let b1 = derive_share_blind(&sk, round_id, 1, van, 1);
assert_ne!(b0, b1);
}
#[test]
fn different_proposal_id_gives_different_values() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let r_p1 = derive_share_randomness(&sk, round_id, 1, van, 0);
let r_p2 = derive_share_randomness(&sk, round_id, 2, van, 0);
assert_ne!(r_p1, r_p2);
}
#[test]
fn different_round_id_gives_different_values() {
let sk = test_sk();
let van = test_van();
let r_a = derive_share_randomness(&sk, pallas::Base::from(1u64), 1, van, 0);
let r_b = derive_share_randomness(&sk, pallas::Base::from(2u64), 1, van, 0);
assert_ne!(r_a, r_b);
}
#[test]
fn randomness_and_blind_differ_for_same_inputs() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let r = derive_share_randomness(&sk, round_id, 1, van, 0);
let b = derive_share_blind(&sk, round_id, 1, van, 0);
assert_ne!(r, b, "domain separation must prevent r == blind");
}
#[test]
fn all_16_shares_are_distinct() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let randoms: Vec<_> = (0..16u8)
.map(|i| derive_share_randomness(&sk, round_id, 1, van, i))
.collect();
let blinds: Vec<_> = (0..16u8)
.map(|i| derive_share_blind(&sk, round_id, 1, van, i))
.collect();
for i in 0..16 {
for j in (i + 1)..16 {
assert_ne!(randoms[i], randoms[j], "r_{} == r_{}", i, j);
assert_ne!(blinds[i], blinds[j], "blind_{} == blind_{}", i, j);
}
}
}
#[test]
fn different_van_commitment_gives_different_values() {
let sk = test_sk();
let round_id = test_round_id();
let van_a = pallas::Base::from(0xAAAA_u64);
let van_b = pallas::Base::from(0xBBBB_u64);
for i in 0..16u8 {
let r_a = derive_share_randomness(&sk, round_id, 1, van_a, i);
let r_b = derive_share_randomness(&sk, round_id, 1, van_b, i);
assert_ne!(r_a, r_b, "r_{} must differ across VANs", i);
let b_a = derive_share_blind(&sk, round_id, 1, van_a, i);
let b_b = derive_share_blind(&sk, round_id, 1, van_b, i);
assert_ne!(b_a, b_b, "blind_{} must differ across VANs", i);
}
}
fn show(label: &str, shares: &[u64; 16]) {
let parts: Vec<String> = shares
.iter()
.map(|&v| {
if v == 0 {
"0".into()
} else if v >= 1_000_000 {
format!("{}M", v / 1_000_000)
} else if v >= 1_000 {
format!("{}K", v / 1_000)
} else {
format!("{}", v)
}
})
.collect();
std::eprintln!(" {}: [{}]", label, parts.join(", "));
}
#[test]
fn denom_split_zero_ballots() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(0, &sk, rid, 1, van);
show("0 ballots", &shares);
assert_eq!(shares, [0; 16]);
}
#[test]
fn denom_split_single_ballot() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(1, &sk, rid, 1, van);
show("1 ballot (0.125 ZEC)", &shares);
assert_eq!(shares[0], 1);
for i in 1..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_sub_zec() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(4, &sk, rid, 1, van);
show("4 ballots (0.5 ZEC)", &shares);
assert_eq!(shares[0..4], [1; 4]);
for i in 4..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_one_zec() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(8, &sk, rid, 1, van);
show("8 ballots (1 ZEC)", &shares);
assert_eq!(shares[0..8], [1; 8]);
for i in 8..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_small_balance() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(50, &sk, rid, 1, van);
show("50 ballots (6.25 ZEC)", &shares);
assert_eq!(shares[0..5], [10; 5]);
for i in 5..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_all_denoms_exact() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(11_111, &sk, rid, 1, van);
show("11,111 ballots (1,388.9 ZEC)", &shares);
assert_eq!(shares[0], 10_000);
assert_eq!(shares[1], 1_000);
assert_eq!(shares[2], 100);
assert_eq!(shares[3], 10);
assert_eq!(shares[4], 1);
for i in 5..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_medium_holder_with_remainder() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(4_800, &sk, rid, 1, van);
show("4,800 ballots (600 ZEC)", &shares);
assert_eq!(shares[0..4], [1_000; 4]);
assert_eq!(shares[4..9], [100; 5]);
let remainder_sum: u64 = shares[9..16].iter().sum();
assert_eq!(remainder_sum, 300);
for i in 9..16 {
assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
}
assert_eq!(shares.iter().sum::<u64>(), 4_800);
}
#[test]
fn denom_split_high_hamming_weight() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(999, &sk, rid, 1, van);
show("999 ballots (124.875 ZEC)", &shares);
assert_eq!(shares[0..9], [100; 9]);
let remainder_sum: u64 = shares[9..16].iter().sum();
assert_eq!(remainder_sum, 99);
for i in 9..16 {
assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
}
}
#[test]
fn denom_split_exact_denomination_match() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(3_000_000, &sk, rid, 1, van);
show("3M ballots (375 ZEC)", &shares);
assert_eq!(shares[0..3], [1_000_000; 3]);
for i in 3..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_8m_ballots() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(8_000_000, &sk, rid, 1, van);
show("8M ballots (1M ZEC)", &shares);
assert_eq!(shares[0..8], [1_000_000; 8]);
for i in 8..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_fills_all_9_denom_slots() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(90_000_000, &sk, rid, 1, van);
show("90M ballots (11.25M ZEC)", &shares);
assert_eq!(shares[0..9], [10_000_000; 9]);
for i in 9..16 {
assert_eq!(shares[i], 0);
}
}
#[test]
fn denom_split_overflow_into_remainder() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(100_000_000, &sk, rid, 1, van);
show("100M ballots (12.5M ZEC)", &shares);
assert_eq!(shares[0..9], [10_000_000; 9]);
let remainder_sum: u64 = shares[9..16].iter().sum();
assert_eq!(remainder_sum, 10_000_000);
for i in 9..16 {
assert!(shares[i] > 0, "remainder slot {} should be non-zero", i);
}
}
#[test]
fn denom_split_mixed_with_remainder() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(1_234_567, &sk, rid, 1, van);
show("1,234,567 ballots (154K ZEC)", &shares);
assert_eq!(shares[0], 1_000_000);
assert_eq!(shares[1..3], [100_000; 2]);
assert_eq!(shares[3..6], [10_000; 3]);
assert_eq!(shares[6..9], [1_000; 3]);
let remainder_sum: u64 = shares[9..16].iter().sum();
assert_eq!(remainder_sum, 1_567);
assert_eq!(shares.iter().sum::<u64>(), 1_234_567);
}
#[test]
fn denom_split_small_remainder_fewer_than_free_slots() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let shares = denomination_split(10_000_003, &sk, rid, 1, van);
show("10,000,003 ballots", &shares);
assert_eq!(shares[0], 10_000_000);
let remainder_sum: u64 = shares[1..16].iter().sum();
assert_eq!(remainder_sum, 3);
assert_eq!(shares.iter().sum::<u64>(), 10_000_003);
}
#[test]
fn denom_split_sum_invariant() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let test_values: [u64; 14] = [
0,
1,
50,
99,
100,
999,
1_000,
10_000,
100_000,
1_000_000,
8_234_567,
20_000_000,
80_000_000,
168_000_000,
];
for &v in &test_values {
let shares = denomination_split(v, &sk, rid, 1, van);
assert_eq!(
shares.iter().sum::<u64>(),
v,
"sum invariant violated for {}",
v
);
}
}
#[test]
fn denom_split_all_shares_in_range() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let test_values: [u64; 8] = [
1,
10_000,
1_000_000,
8_234_567,
15_000_000,
20_000_000,
80_000_000,
168_000_000,
];
for &v in &test_values {
let shares = denomination_split(v, &sk, rid, 1, van);
for (i, &s) in shares.iter().enumerate() {
assert!(
s < (1u64 << 30),
"share {} = {} exceeds 2^30 for {}",
i,
s,
v
);
}
}
}
#[test]
fn remainder_is_deterministic() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let a = denomination_split(999, &sk, rid, 1, van);
let b = denomination_split(999, &sk, rid, 1, van);
assert_eq!(a, b);
}
#[test]
fn remainder_differs_across_proposals() {
let sk = test_sk();
let rid = test_round_id();
let van = test_van();
let a = denomination_split(999, &sk, rid, 1, van);
let b = denomination_split(999, &sk, rid, 2, van);
show("999 ballots, proposal 1", &a);
show("999 ballots, proposal 2", &b);
assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
assert_ne!(
a[9..16],
b[9..16],
"remainder should differ across proposals"
);
}
#[test]
fn remainder_differs_across_vans() {
let sk = test_sk();
let rid = test_round_id();
let van_a = pallas::Base::from(0xAAAA_u64);
let van_b = pallas::Base::from(0xBBBB_u64);
let a = denomination_split(999, &sk, rid, 1, van_a);
let b = denomination_split(999, &sk, rid, 1, van_b);
show("999 ballots, VAN A", &a);
show("999 ballots, VAN B", &b);
assert_eq!(a[0..9], b[0..9], "denomination slots should be identical");
assert_ne!(a[9..16], b[9..16], "remainder should differ across VANs");
}
#[test]
fn shuffle_preserves_sum() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let mut shares = denomination_split(8_234_567, &sk, round_id, 1, van);
let sum_before = shares.iter().sum::<u64>();
deterministic_shuffle(&mut shares, &sk, round_id, 1, van);
assert_eq!(shares.iter().sum::<u64>(), sum_before);
}
#[test]
fn shuffle_preserves_multiset() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let original = denomination_split(4_800, &sk, round_id, 1, van);
let mut shuffled = original;
deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
let mut sorted_orig = original;
sorted_orig.sort();
let mut sorted_shuf = shuffled;
sorted_shuf.sort();
assert_eq!(sorted_orig, sorted_shuf, "shuffle must be a permutation");
}
#[test]
fn shuffle_is_deterministic() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let mut a = denomination_split(4_800, &sk, round_id, 1, van);
let mut b = denomination_split(4_800, &sk, round_id, 1, van);
deterministic_shuffle(&mut a, &sk, round_id, 1, van);
deterministic_shuffle(&mut b, &sk, round_id, 1, van);
assert_eq!(a, b, "same inputs must produce same permutation");
}
#[test]
fn shuffle_differs_across_proposals() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let mut a = denomination_split(4_800, &sk, round_id, 1, van);
let mut b = denomination_split(4_800, &sk, round_id, 1, van);
deterministic_shuffle(&mut a, &sk, round_id, 1, van);
deterministic_shuffle(&mut b, &sk, round_id, 2, van);
assert_ne!(
a, b,
"different proposals should produce different permutations"
);
}
#[test]
fn shuffle_differs_across_vans() {
let sk = test_sk();
let round_id = test_round_id();
let van_a = pallas::Base::from(0xAAAA_u64);
let van_b = pallas::Base::from(0xBBBB_u64);
let mut a = denomination_split(4_800, &sk, round_id, 1, van_a);
let mut b = denomination_split(4_800, &sk, round_id, 1, van_b);
deterministic_shuffle(&mut a, &sk, round_id, 1, van_a);
deterministic_shuffle(&mut b, &sk, round_id, 1, van_b);
assert_ne!(a, b, "different VANs should produce different permutations");
}
#[test]
fn shuffle_actually_reorders() {
let sk = test_sk();
let round_id = test_round_id();
let van = test_van();
let original = denomination_split(4_800, &sk, round_id, 1, van);
let mut shuffled = original;
deterministic_shuffle(&mut shuffled, &sk, round_id, 1, van);
assert_ne!(
original, shuffled,
"shuffle should reorder (vanishingly unlikely to be identity for 12 non-zero shares)"
);
}
}