use crate::signing::{bind_to_election, canonicalised_ring, normalise_election_id};
use crate::types::{KeyImage, PublicKey, Signature};
use curve25519_dalek::ristretto::RistrettoPoint;
pub fn verify_vote(
vote: &[u8],
election_id: &str,
signature: &Signature,
key_image: &KeyImage,
ring: &[PublicKey],
) -> bool {
if vote.is_empty() || election_id.is_empty() {
return false;
}
let normalized = normalise_election_id(election_id);
let election_id = normalized.as_bytes();
let sorted = match canonicalised_ring(ring) {
Ok(s) => s,
Err(_) => return false,
};
if signature.responses.len() != sorted.len() {
return false;
}
let ring_points: Vec<RistrettoPoint> = sorted.iter().map(|pk| pk.point).collect();
let bound = bind_to_election(election_id, vote);
crate::blsag::verify(
signature.challenge,
&signature.responses,
key_image.point,
&ring_points,
election_id,
&bound,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::generate_identity;
use crate::signing::sign_vote;
const EID: &str = "election-2026";
fn fresh_ring(n: usize) -> (crate::types::SecretKey, Vec<PublicKey>) {
let voter = generate_identity();
let mut ring = vec![voter.public_key];
for _ in 1..n {
ring.push(generate_identity().public_key);
}
(voter.secret_key, ring)
}
#[test]
fn round_trip_succeeds() {
let (sk, ring) = fresh_ring(5);
let proof = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
assert!(verify_vote(
b"option-A",
EID,
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn round_trip_with_minimum_ring_size() {
let (sk, ring) = fresh_ring(2);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
assert!(verify_vote(
b"yes",
EID,
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn ring_order_does_not_matter() {
let (sk, ring) = fresh_ring(4);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
let mut reversed = ring.clone();
reversed.reverse();
assert!(verify_vote(
b"yes",
EID,
&proof.signature,
&proof.key_image,
&reversed
));
}
#[test]
fn tampered_vote_is_rejected() {
let (sk, ring) = fresh_ring(4);
let proof = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
assert!(!verify_vote(
b"option-B",
EID,
&proof.signature,
&proof.key_image,
&ring,
));
}
#[test]
fn tampered_signature_is_rejected() {
let (sk, ring) = fresh_ring(4);
let mut proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
proof.signature.responses[0] = curve25519_dalek::scalar::Scalar::ZERO;
assert!(!verify_vote(
b"yes",
EID,
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn swapped_key_image_is_rejected() {
let (sk_a, mut ring) = fresh_ring(3);
let voter_b = generate_identity();
ring.push(voter_b.public_key);
let proof_a = sign_vote(&sk_a, b"yes", EID, &ring).unwrap();
let proof_b = sign_vote(&voter_b.secret_key, b"yes", EID, &ring).unwrap();
assert!(!verify_vote(
b"yes",
EID,
&proof_a.signature,
&proof_b.key_image,
&ring
));
}
#[test]
fn wrong_ring_is_rejected() {
let (sk, ring) = fresh_ring(3);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
let mut other_ring = ring.clone();
other_ring[1] = generate_identity().public_key;
assert!(!verify_vote(
b"yes",
EID,
&proof.signature,
&proof.key_image,
&other_ring
));
}
#[test]
fn wrong_election_id_is_rejected() {
let (sk, ring) = fresh_ring(3);
let proof = sign_vote(&sk, b"yes", "election-X", &ring).unwrap();
assert!(!verify_vote(
b"yes",
"election-Y",
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn empty_inputs_at_verification_are_rejected() {
let (sk, ring) = fresh_ring(3);
let proof = sign_vote(&sk, b"yes", EID, &ring).unwrap();
assert!(!verify_vote(
b"",
EID,
&proof.signature,
&proof.key_image,
&ring
));
assert!(!verify_vote(
b"yes",
"",
&proof.signature,
&proof.key_image,
&ring
));
}
#[test]
fn same_voter_yields_same_tag() {
let (sk, ring) = fresh_ring(3);
let p1 = sign_vote(&sk, b"option-A", EID, &ring).unwrap();
let p2 = sign_vote(&sk, b"option-B", EID, &ring).unwrap();
assert_eq!(p1.key_image, p2.key_image);
}
#[test]
fn same_voter_yields_different_tags_across_elections() {
let (sk, ring) = fresh_ring(3);
let p1 = sign_vote(&sk, b"option-A", "election-A", &ring).unwrap();
let p2 = sign_vote(&sk, b"option-A", "election-B", &ring).unwrap();
assert_ne!(p1.key_image, p2.key_image);
}
#[test]
fn different_voters_yield_different_tags() {
let (sk_a, mut ring) = fresh_ring(3);
let voter_b = generate_identity();
ring.push(voter_b.public_key);
let p_a = sign_vote(&sk_a, b"yes", EID, &ring).unwrap();
let p_b = sign_vote(&voter_b.secret_key, b"yes", EID, &ring).unwrap();
assert_ne!(p_a.key_image, p_b.key_image);
}
}