use ff::{Field, PrimeField};
use group::{Curve, GroupEncoding};
use pasta_curves::pallas;
use orchard::keys::SpendingKey;
use voting_circuits::vote_proof::{build_vote_proof_from_delegation, VOTE_COMM_TREE_DEPTH};
use crate::types::{
ct_option_to_result, validate_vote_decision, EncryptedShare, ProofProgressReporter,
VoteCommitmentBundle, VotingError,
};
const VOTE_PROOF_STACK_BYTES: usize = 64 * 1024 * 1024;
#[allow(clippy::too_many_arguments)]
pub fn build_vote_commitment(
hotkey_seed: &[u8],
network_id: u32,
address_index: u32,
total_note_value: u64,
gov_comm_rand: &[u8],
voting_round_id: &[u8],
ea_pk: &[u8],
proposal_id: u32,
choice: u32,
num_options: u32,
van_auth_path: &[[u8; 32]],
van_position: u32,
anchor_height: u32,
proposal_authority: u64,
single_share: bool,
progress: &dyn ProofProgressReporter,
) -> Result<VoteCommitmentBundle, VotingError> {
validate_vote_decision(choice, num_options)?;
if proposal_id < 1 || proposal_id > 15 {
return Err(VotingError::InvalidInput {
message: format!(
"proposal_id must be 1..15 (1-indexed, matching on-chain IDs; 0 is the circuit sentinel), got {}",
proposal_id
),
});
}
if van_auth_path.len() != VOTE_COMM_TREE_DEPTH {
return Err(VotingError::InvalidInput {
message: format!(
"van_auth_path must have {} siblings, got {}",
VOTE_COMM_TREE_DEPTH,
van_auth_path.len()
),
});
}
progress.on_progress(0.05);
let sk = derive_spending_key(hotkey_seed, network_id)?;
let gcr_bytes: [u8; 32] = gov_comm_rand.try_into().map_err(|_| VotingError::InvalidInput {
message: format!(
"gov_comm_rand must be 32 bytes, got {}",
gov_comm_rand.len()
),
})?;
let gcr = ct_option_to_result(
pallas::Base::from_repr(gcr_bytes),
"gov_comm_rand is not a valid Pallas field element",
)?;
let vri_bytes: [u8; 32] = voting_round_id.try_into().map_err(|_| VotingError::InvalidInput {
message: format!(
"voting_round_id must be 32 bytes, got {}",
voting_round_id.len()
),
})?;
let vri = ct_option_to_result(
pallas::Base::from_repr(vri_bytes),
"voting_round_id is not a canonical Pallas Fp element",
)?;
let ea_pk_bytes: [u8; 32] = ea_pk.try_into().map_err(|_| VotingError::InvalidInput {
message: format!("ea_pk must be 32 bytes, got {}", ea_pk.len()),
})?;
let ea_pk_point: pallas::Point = Option::from(pallas::Point::from_bytes(&ea_pk_bytes))
.ok_or_else(|| VotingError::InvalidInput {
message: "ea_pk is not a valid compressed Pallas point".to_string(),
})?;
let ea_pk_affine = ea_pk_point.to_affine();
let mut auth_path = [pallas::Base::zero(); VOTE_COMM_TREE_DEPTH];
for (i, sibling) in van_auth_path.iter().enumerate() {
auth_path[i] = ct_option_to_result(
pallas::Base::from_repr(*sibling),
&format!("van_auth_path[{}] is not a valid Pallas field element", i),
)?;
}
progress.on_progress(0.10);
let alpha_v = pallas::Scalar::random(&mut rand::thread_rng());
let sk_for_proof = sk.clone();
let vote_bundle = std::thread::Builder::new()
.name("vote-proof-build".to_string())
.stack_size(VOTE_PROOF_STACK_BYTES)
.spawn(move || {
build_vote_proof_from_delegation(
&sk_for_proof,
address_index,
total_note_value,
gcr,
vri,
auth_path,
van_position,
anchor_height,
proposal_id as u64,
choice as u64,
ea_pk_affine,
alpha_v,
proposal_authority,
single_share,
)
})
.map_err(|e| VotingError::Internal {
message: format!("failed to spawn vote proof builder thread: {e}"),
})?
.join()
.map_err(|_| VotingError::Internal {
message: "vote proof builder thread panicked".to_string(),
})?
.map_err(|e| VotingError::ProofFailed {
message: format!("vote proof generation failed: {}", e),
})?;
progress.on_progress(1.0);
let van_nullifier = vote_bundle.instance.van_nullifier.to_repr().to_vec();
let van_new = vote_bundle.instance.vote_authority_note_new.to_repr().to_vec();
let vote_commitment = vote_bundle.instance.vote_commitment.to_repr().to_vec();
let enc_shares: Vec<EncryptedShare> = vote_bundle
.encrypted_shares
.iter()
.map(|es| EncryptedShare {
c1: es.c1.to_vec(),
c2: es.c2.to_vec(),
share_index: es.share_index,
plaintext_value: es.plaintext_value,
randomness: es.randomness.to_vec(),
})
.collect();
Ok(VoteCommitmentBundle {
van_nullifier,
vote_authority_note_new: van_new,
vote_commitment,
proposal_id,
proof: vote_bundle.proof,
enc_shares,
anchor_height,
vote_round_id: hex::encode(voting_round_id),
shares_hash: vote_bundle.shares_hash.to_repr().to_vec(),
share_blinds: vote_bundle.share_blinds.iter().map(|b| b.to_repr().to_vec()).collect(),
share_comms: vote_bundle.share_comms.iter().map(|c| c.to_repr().to_vec()).collect(),
r_vpk_bytes: vote_bundle.r_vpk_bytes.to_vec(),
alpha_v: alpha_v.to_repr().to_vec(),
})
}
pub fn derive_spending_key(hotkey_seed: &[u8], network_id: u32) -> Result<SpendingKey, VotingError> {
use zcash_keys::keys::UnifiedSpendingKey;
use zcash_protocol::consensus::{MAIN_NETWORK, TEST_NETWORK};
use zip32::AccountId;
if hotkey_seed.len() < 32 {
return Err(VotingError::InvalidInput {
message: format!(
"hotkey_seed must be at least 32 bytes, got {}",
hotkey_seed.len()
),
});
}
let account = AccountId::try_from(0u32).expect("account 0 is valid");
let usk = match network_id {
0 => UnifiedSpendingKey::from_seed(&TEST_NETWORK, hotkey_seed, account),
1 => UnifiedSpendingKey::from_seed(&MAIN_NETWORK, hotkey_seed, account),
_ => {
return Err(VotingError::InvalidInput {
message: format!(
"invalid network_id {}, expected 0 (testnet) or 1 (mainnet)",
network_id
),
});
}
}
.map_err(|e| VotingError::InvalidInput {
message: format!("failed to derive UnifiedSpendingKey from hotkey_seed: {}", e),
})?;
let sk: SpendingKey = *usk.orchard();
Ok(sk)
}
#[cfg(test)]
mod tests {
use super::*;
struct TestReporter;
impl ProofProgressReporter for TestReporter {
fn on_progress(&self, _progress: f64) {}
}
#[test]
fn test_build_vote_commitment_bad_choice() {
assert!(build_vote_commitment(
&[0x42; 64],
1,
0,
1_000_000,
&[0u8; 32],
&[0u8; 32],
&[0u8; 32],
1,
3, 2,
&[[0u8; 32]; 24],
0,
1,
65535,
false,
&TestReporter,
)
.is_err());
}
#[test]
fn test_build_vote_commitment_proposal_id_zero_rejected() {
assert!(build_vote_commitment(
&[0x42; 64],
1,
0,
1_000_000,
&[0u8; 32],
&[0u8; 32],
&[0u8; 32],
0, 0,
2,
&[[0u8; 32]; 24],
0,
1,
65535,
false,
&TestReporter,
)
.is_err());
}
#[test]
fn test_build_vote_commitment_proposal_id_too_large() {
assert!(build_vote_commitment(
&[0x42; 64],
1,
0,
1_000_000,
&[0u8; 32],
&[0u8; 32],
&[0u8; 32],
16, 0,
2,
&[[0u8; 32]; 24],
0,
1,
65535,
false,
&TestReporter,
)
.is_err());
}
#[test]
fn test_build_vote_commitment_wrong_auth_path_len() {
assert!(build_vote_commitment(
&[0x42; 64],
1,
0,
1_000_000,
&[0u8; 32],
&[0u8; 32],
&[0u8; 32],
1,
0,
2,
&[[0u8; 32]; 10], 0,
1,
65535,
false,
&TestReporter,
)
.is_err());
}
}