diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 303a0a748b9..2f3da07262a 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -10,6 +10,7 @@ Arissara asyncio Attributes auditability +babystep backpressure bech bimap @@ -34,6 +35,7 @@ Chotivichit chrono cids ciphertext +ciphertexts codegen codepoints coti @@ -83,6 +85,7 @@ futimens genhtml GETFL getres +giantstep gmtime gossipsub happ diff --git a/rust/catalyst-voting/Cargo.toml b/rust/catalyst-voting/Cargo.toml index ecb47fecc26..9044c3ce5e5 100644 --- a/rust/catalyst-voting/Cargo.toml +++ b/rust/catalyst-voting/Cargo.toml @@ -11,9 +11,12 @@ license.workspace = true workspace = true [dependencies] -anyhow = "1.0.71" +thiserror = "1.0.56" rand_core = "0.6.4" curve25519-dalek = { version = "4.0" } [dev-dependencies] -proptest = {version = "1.5.0", features = ["attr-macro"] } +proptest = {version = "1.5.0" } +# Potentially it could be replaced with using `proptest::property_test` attribute macro, +# after this PR will be merged https://github.com/proptest-rs/proptest/pull/523 +test-strategy = "0.4.0" diff --git a/rust/catalyst-voting/src/crypto/elgamal.rs b/rust/catalyst-voting/src/crypto/elgamal.rs index ea7669823c2..c33ccfc2db7 100644 --- a/rust/catalyst-voting/src/crypto/elgamal.rs +++ b/rust/catalyst-voting/src/crypto/elgamal.rs @@ -1,7 +1,7 @@ //! Implementation of the lifted ``ElGamal`` crypto system, and combine with `ChaCha` //! stream cipher to produce a hybrid encryption scheme. -use std::ops::Mul; +use std::ops::{Add, Mul}; use rand_core::CryptoRngCore; @@ -21,20 +21,29 @@ pub struct Ciphertext(GroupElement, GroupElement); impl SecretKey { /// Generate a random `SecretKey` value from the random number generator. - pub fn random(rng: &mut R) -> Self { + pub fn generate(rng: &mut R) -> Self { Self(Scalar::random(rng)) } /// Generate a corresponding `PublicKey`. + #[must_use] pub fn public_key(&self) -> PublicKey { PublicKey(GroupElement::GENERATOR.mul(&self.0)) } } +impl Ciphertext { + /// Generate a zero `Ciphertext`. + /// The same as encrypt a `Scalar::zero()` message and `Scalar::zero()` randomness. + pub(crate) fn zero() -> Self { + Ciphertext(GroupElement::zero(), GroupElement::zero()) + } +} + /// Given a `message` represented as a `Scalar`, return a ciphertext using the /// lifted ``ElGamal`` mechanism. /// Returns a ciphertext of type `Ciphertext`. -pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { +pub(crate) fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> Ciphertext { let e1 = GroupElement::GENERATOR.mul(randomness); let e2 = &GroupElement::GENERATOR.mul(message) + &public_key.0.mul(randomness); Ciphertext(e1, e2) @@ -42,17 +51,33 @@ pub fn encrypt(message: &Scalar, public_key: &PublicKey, randomness: &Scalar) -> /// Decrypt ``ElGamal`` `Ciphertext`, returns the original message represented as a /// `GroupElement`. -pub fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { +pub(crate) fn decrypt(cipher: &Ciphertext, secret_key: &SecretKey) -> GroupElement { &(&cipher.0 * &secret_key.0.negate()) + &cipher.1 } +impl Mul<&Scalar> for &Ciphertext { + type Output = Ciphertext; + + fn mul(self, rhs: &Scalar) -> Self::Output { + Ciphertext(&self.0 * rhs, &self.1 * rhs) + } +} + +impl Add<&Ciphertext> for &Ciphertext { + type Output = Ciphertext; + + fn add(self, rhs: &Ciphertext) -> Self::Output { + Ciphertext(&self.0 + &rhs.0, &self.1 + &rhs.1) + } +} + #[cfg(test)] mod tests { use proptest::{ arbitrary::any, prelude::{Arbitrary, BoxedStrategy, Strategy}, - property_test, }; + use test_strategy::proptest; use super::*; @@ -65,7 +90,29 @@ mod tests { } } - #[property_test] + #[proptest] + fn ciphertext_add_test(e1: Scalar, e2: Scalar, e3: Scalar, e4: Scalar) { + let g1 = GroupElement::GENERATOR.mul(&e1); + let g2 = GroupElement::GENERATOR.mul(&e2); + let c1 = Ciphertext(g1.clone(), g2.clone()); + + let g3 = GroupElement::GENERATOR.mul(&e3); + let g4 = GroupElement::GENERATOR.mul(&e4); + let c2 = Ciphertext(g3.clone(), g4.clone()); + + assert_eq!(&c1 + &c2, Ciphertext(&g1 + &g3, &g2 + &g4)); + } + + #[proptest] + fn ciphertext_mul_test(e1: Scalar, e2: Scalar, e3: Scalar) { + let g1 = GroupElement::GENERATOR.mul(&e1); + let g2 = GroupElement::GENERATOR.mul(&e2); + let c1 = Ciphertext(g1.clone(), g2.clone()); + + assert_eq!(&c1 * &e3, Ciphertext(&g1 * &e3, &g2 * &e3)); + } + + #[proptest] fn elgamal_encryption_decryption_test( secret_key: SecretKey, message: Scalar, randomness: Scalar, ) { diff --git a/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs new file mode 100644 index 00000000000..35bc38ada09 --- /dev/null +++ b/rust/catalyst-voting/src/crypto/group/babystep_giantstep.rs @@ -0,0 +1,123 @@ +//! Implementation of baby-step giant-step algorithm to solve the discrete logarithm over +//! for the Ristretto255 group. + +use std::collections::HashMap; + +use super::{GroupElement, Scalar}; + +/// Default balance value. +/// Make steps asymmetric, in order to better use caching of baby steps. +/// Balance of 2 means that baby steps are 2 time more than `sqrt(max_votes)` +const DEFAULT_BALANCE: u64 = 2; + +/// Holds precomputed baby steps `table` for the baby-step giant-step algorithm +/// for solving discrete log. +#[derive(Debug, Clone)] +pub struct BabyStepGiantStep { + /// Table of baby step precomputed values + table: HashMap, + /// baby step size value + baby_step_size: u64, + /// giant step value + giant_step: GroupElement, +} + +#[derive(thiserror::Error, Debug)] +pub enum BabyStepError { + /// Invalid max value or balance + #[error("Maximum value and balance must be greater than zero, provided max value: {0} and balance: {1}.")] + InvalidMaxValueOrBalance(u64, u64), + /// Max value exceeded + #[error("Max log value exceeded. Means that the actual discrete log for the provided group element is higher than the provided `max_log_value`.")] + MaxLogExceeded, +} + +impl BabyStepGiantStep { + /// Creates a new setup for the baby-step giant-step algorithm. + /// + /// Balance is used to make steps asymmetrical. If the table is reused multiple times + /// with the same `max_value` it is recommended to set a balance > 1, since this + /// will allow to cache more results, at the expense of a higher memory footprint. + /// + /// If not provided it will default to 2, means that the table will precompute 2 times + /// more baby steps than the standard O(sqrt(n)), 1 means symmetrical steps. + /// + /// + /// **NOTE** It is a heavy operation, so pls reuse the same instance for performing + /// `baby_step_giant_step` function for the same `max_value`. + /// + /// # Errors + /// - `BabyStepError` + pub fn new(max_log_value: u64, balance: Option) -> Result { + let balance = balance.unwrap_or(DEFAULT_BALANCE); + + if balance == 0 || max_log_value == 0 { + return Err(BabyStepError::InvalidMaxValueOrBalance( + max_log_value, + balance, + )); + } + + #[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss + )] + let sqrt_step_size = (max_log_value as f64).sqrt().ceil() as u64; + let baby_step_size = sqrt_step_size * balance; + let mut table = HashMap::new(); + + let mut e = GroupElement::zero(); + for baby_step in 0..=baby_step_size { + let new_e = &e + &GroupElement::GENERATOR; + table.insert(e, baby_step); + e = new_e; + } + + let giant_step = &GroupElement::GENERATOR * &Scalar::from(baby_step_size).negate(); + Ok(Self { + table, + baby_step_size, + giant_step, + }) + } + + /// Solve the discrete log using baby step giant step algorithm. + /// + /// # Errors + /// - `BabyStepError` + pub fn discrete_log(&self, mut point: GroupElement) -> Result { + for baby_step in 0..=self.baby_step_size { + if let Some(x) = self.table.get(&point) { + let r = baby_step * self.baby_step_size + x; + return Ok(r); + } + point = &point + &self.giant_step; + } + // If we get here, the point is not in the table + // So we exceeded the maximum value of the discrete log + Err(BabyStepError::MaxLogExceeded) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Mul; + + use test_strategy::proptest; + + use super::*; + + // Starting `max_log_value` from 2 allows to eliminate possible `Invalid use of empty + // range 1..1` for `log` strategy + #[proptest] + fn baby_step_giant_step_test( + #[strategy(2..10000u64)] max_log_value: u64, #[strategy(1..#max_log_value)] log: u64, + ) { + let ge = GroupElement::GENERATOR.mul(&Scalar::from(log)); + + let baby_step_giant_step = BabyStepGiantStep::new(max_log_value, None).unwrap(); + let result = baby_step_giant_step.discrete_log(ge).unwrap(); + assert_eq!(result, log); + } +} diff --git a/rust/catalyst-voting/src/crypto/group/mod.rs b/rust/catalyst-voting/src/crypto/group/mod.rs index 41c8c61592f..afea38ca878 100644 --- a/rust/catalyst-voting/src/crypto/group/mod.rs +++ b/rust/catalyst-voting/src/crypto/group/mod.rs @@ -1,7 +1,8 @@ //! Group definitions used in voting protocol. //! For more information, see: +mod babystep_giantstep; mod ristretto255; -#[allow(clippy::module_name_repetitions)] -pub use ristretto255::{GroupElement, Scalar}; +pub(crate) use babystep_giantstep::BabyStepGiantStep; +pub(crate) use ristretto255::{GroupElement, Scalar}; diff --git a/rust/catalyst-voting/src/crypto/group/ristretto255.rs b/rust/catalyst-voting/src/crypto/group/ristretto255.rs index aa8b9df54ba..3f2a718dba6 100644 --- a/rust/catalyst-voting/src/crypto/group/ristretto255.rs +++ b/rust/catalyst-voting/src/crypto/group/ristretto255.rs @@ -2,7 +2,10 @@ // cspell: words BASEPOINT -use std::ops::{Add, Mul, Sub}; +use std::{ + hash::Hash, + ops::{Add, Mul, Sub}, +}; use curve25519_dalek::{ constants::{RISTRETTO_BASEPOINT_POINT, RISTRETTO_BASEPOINT_TABLE}, @@ -26,6 +29,12 @@ impl From for Scalar { } } +impl Hash for GroupElement { + fn hash(&self, state: &mut H) { + self.0.compress().as_bytes().hash(state); + } +} + impl Scalar { /// Generate a random scalar value from the random number generator. pub fn random(rng: &mut R) -> Self { @@ -44,6 +53,11 @@ impl Scalar { Scalar(IScalar::ONE) } + /// Increment on `1`. + pub fn increment(&mut self) { + self.0 += IScalar::ONE; + } + /// negative value pub fn negate(&self) -> Self { Scalar(-self.0) @@ -124,8 +138,8 @@ mod tests { use proptest::{ arbitrary::any, prelude::{Arbitrary, BoxedStrategy, Strategy}, - property_test, }; + use test_strategy::proptest; use super::*; @@ -138,7 +152,7 @@ mod tests { } } - #[property_test] + #[proptest] fn scalar_arithmetic_tests(e1: Scalar, e2: Scalar, e3: Scalar) { assert_eq!(&(&e1 + &e2) + &e3, &e1 + &(&e2 + &e3)); assert_eq!(&e1 + &e2, &e2 + &e1); @@ -150,7 +164,7 @@ mod tests { assert_eq!(&(&e1 + &e2) * &e3, &(&e1 * &e3) + &(&e2 * &e3)); } - #[property_test] + #[proptest] fn group_element_arithmetic_tests(e1: Scalar, e2: Scalar) { let ge = GroupElement::GENERATOR.mul(&e1); assert_eq!(&GroupElement::zero() + &ge, ge); diff --git a/rust/catalyst-voting/src/crypto/mod.rs b/rust/catalyst-voting/src/crypto/mod.rs index d168772ac0a..16822f6efed 100644 --- a/rust/catalyst-voting/src/crypto/mod.rs +++ b/rust/catalyst-voting/src/crypto/mod.rs @@ -1,4 +1,4 @@ //! Crypto primitives which are used by voting protocol. -mod elgamal; -mod group; +pub(crate) mod elgamal; +pub(crate) mod group; diff --git a/rust/catalyst-voting/src/lib.rs b/rust/catalyst-voting/src/lib.rs index 63f24bcaadf..ef3e7456cde 100644 --- a/rust/catalyst-voting/src/lib.rs +++ b/rust/catalyst-voting/src/lib.rs @@ -1,58 +1,83 @@ //! Voting primitives which are used among Catalyst ecosystem. - -#![allow(dead_code, unused_variables, clippy::todo)] +//! +//! ```rust +//! use catalyst_voting::{ +//! decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, +//! Vote, +//! }; +//! +//! struct Voter { +//! voting_power: u64, +//! choice: usize, +//! } +//! +//! let mut rng = rand_core::OsRng; +//! let voting_options = 3; +//! let election_secret_key = SecretKey::generate(&mut rng); +//! let election_public_key = election_secret_key.public_key(); +//! +//! let voter_1 = Voter { +//! voting_power: 10, +//! choice: 0, +//! }; +//! +//! let voter_2 = Voter { +//! voting_power: 20, +//! choice: 1, +//! }; +//! +//! let voter_3 = Voter { +//! voting_power: 30, +//! choice: 2, +//! }; +//! +//! let vote_1 = Vote::new(voter_1.choice, voting_options).unwrap(); +//! let vote_2 = Vote::new(voter_2.choice, voting_options).unwrap(); +//! let vote_3 = Vote::new(voter_3.choice, voting_options).unwrap(); +//! +//! let voter_1_randomness = EncryptionRandomness::generate(&mut rng, voting_options); +//! let voter_2_randomness = EncryptionRandomness::generate(&mut rng, voting_options); +//! let voter_3_randomness = EncryptionRandomness::generate(&mut rng, voting_options); +//! +//! let encrypted_vote_1 = +//! encrypt_vote(&vote_1, &election_public_key, &voter_1_randomness).unwrap(); +//! let encrypted_vote_2 = +//! encrypt_vote(&vote_2, &election_public_key, &voter_2_randomness).unwrap(); +//! let encrypted_vote_3 = +//! encrypt_vote(&vote_3, &election_public_key, &voter_3_randomness).unwrap(); +//! let encrypted_votes = vec![encrypted_vote_1, encrypted_vote_2, encrypted_vote_3]; +//! +//! let encrypted_tallies: Vec<_> = (0..voting_options) +//! .map(|voting_option| { +//! tally(voting_option, &encrypted_votes, &[ +//! voter_1.voting_power, +//! voter_2.voting_power, +//! voter_3.voting_power, +//! ]) +//! .unwrap() +//! }) +//! .collect(); +//! +//! let decryption_tally_setup = DecryptionTallySetup::new( +//! voter_1.voting_power + voter_2.voting_power + voter_3.voting_power, +//! ) +//! .unwrap(); +//! let decrypted_tallies: Vec<_> = encrypted_tallies +//! .iter() +//! .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) +//! .collect(); +//! +//! assert_eq!(decrypted_tallies, vec![ +//! voter_1.voting_power, +//! voter_2.voting_power, +//! voter_3.voting_power +//! ]); +//! ``` mod crypto; +pub mod tally; +pub mod voter; -/// A representation of the voting choice. -pub struct Vote; - -/// Generate a vote. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) -/// -/// # Errors -/// - TODO -pub fn vote(vote: usize, voting_options: usize) -> anyhow::Result { - todo!() -} - -/// A representation of the encrypted vote. -pub struct EncryptedVote; - -/// Election public key. -pub struct ElectionPublicKey; - -/// Encrypt vote function. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) -/// -/// # Errors -/// - TODO -pub fn encrypt_vote(vote: &Vote, election_pk: &ElectionPublicKey) -> anyhow::Result { - todo!() -} - -/// Vote proof struct. -pub struct VoteProof; - -/// Generates a vote proof, which proofs that the given encrypted vote was correctly -/// generated. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) -/// -/// # Errors -/// - TODO -pub fn generate_vote_proof( - vote: &Vote, encrypted_vote: &EncryptedVote, election_pk: &ElectionPublicKey, -) -> anyhow::Result { - todo!() -} - -/// Verifies a vote proof, is it valid or not for the given encrypted vote. -/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voters-proof) -/// -/// # Errors -/// - TODO -pub fn verify_vote( - vote: &Vote, proof: &VoteProof, election_pk: &ElectionPublicKey, -) -> anyhow::Result<()> { - todo!() -} +pub use crypto::elgamal::{PublicKey, SecretKey}; +pub use tally::{decrypt_tally, tally, DecryptionTallySetup}; +pub use voter::{encrypt_vote, EncryptionRandomness, Vote}; diff --git a/rust/catalyst-voting/src/tally.rs b/rust/catalyst-voting/src/tally.rs new file mode 100644 index 00000000000..a6ef55a02b7 --- /dev/null +++ b/rust/catalyst-voting/src/tally.rs @@ -0,0 +1,124 @@ +//! Module containing all primitives related to the tally process. + +use std::ops::{Add, Mul}; + +use crate::{ + crypto::{ + elgamal::{decrypt, Ciphertext, SecretKey}, + group::{BabyStepGiantStep, Scalar}, + }, + voter::EncryptedVote, +}; + +/// An important decryption tally setup, which holds an important precomputed data needed +/// for decryption. +pub struct DecryptionTallySetup { + /// `BabyStepGiantStep` setup + discrete_log_setup: BabyStepGiantStep, +} + +/// A representation of the encrypted tally. +#[allow(clippy::module_name_repetitions)] +pub struct EncryptedTally(Ciphertext); + +/// Tally error +#[derive(thiserror::Error, Debug)] +pub enum DecryptionTallySetupError { + /// Votes and voting power mismatch + #[error("Total voting power must more than 0.")] + InvalidTotalVotingPowerAmount, +} + +impl DecryptionTallySetup { + /// Generate a decryption tally setup. + /// `total_voting_power` must be a total sum of all voting powers used in the `tally` + /// procedure. + /// + /// **NOTE** It is a heavy operation, so please reuse the same instance for performing + /// `decrypt_tally` function for the same `voting_powers`. + /// + /// # Errors + /// - `DecryptionTallySetupError` + pub fn new(total_voting_power: u64) -> Result { + let discrete_log_setup = BabyStepGiantStep::new(total_voting_power, None) + .map_err(|_| DecryptionTallySetupError::InvalidTotalVotingPowerAmount)?; + Ok(Self { discrete_log_setup }) + } +} + +/// Tally error +#[derive(thiserror::Error, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum TallyError { + /// Votes and voting power mismatch + #[error("Votes and voting power mismatch. Votes amount: {0}. Voting powers amount: {1}.")] + VotingPowerAndVotesMismatch(usize, usize), + /// Invalid encrypted vote + #[error("Invalid encrypted vote at index {0}. Does not have a ciphertext for the voting option {1}.")] + InvalidEncryptedVote(usize, usize), +} + +/// Tally function. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#homomorphic-tally) +/// +/// # Errors +/// - `TallyError` +pub fn tally( + voting_option: usize, votes: &[EncryptedVote], voting_powers: &[u64], +) -> Result { + if votes.len() != voting_powers.len() { + return Err(TallyError::VotingPowerAndVotesMismatch( + votes.len(), + voting_powers.len(), + )); + } + + let mut ciphertexts_per_voting_option = Vec::new(); + for (i, vote) in votes.iter().enumerate() { + let ciphertext = vote + .get_ciphertext_for_choice(voting_option) + .ok_or(TallyError::InvalidEncryptedVote(i, voting_option))?; + ciphertexts_per_voting_option.push(ciphertext); + } + + let zero_ciphertext = Ciphertext::zero(); + + let res = ciphertexts_per_voting_option + .iter() + .zip(voting_powers.iter()) + .map(|(ciphertext, voting_power)| { + let voting_power_scalar = Scalar::from(*voting_power); + ciphertext.mul(&voting_power_scalar) + }) + .fold(zero_ciphertext, |acc, c| acc.add(&c)); + + Ok(EncryptedTally(res)) +} + +/// Tally error +#[derive(thiserror::Error, Debug)] +pub enum DecryptTallyError { + /// Cannot decrypt tally result + #[error( + "Cannot decrypt tally result. Provided an invalid secret key or invalid encrypted tally result." + )] + CannotDecryptTallyResult, +} + +/// Decrypts the encrypted tally result. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#tally-decryption) +/// +/// # Errors +/// - `DecryptTallyError` +#[allow(clippy::module_name_repetitions)] +pub fn decrypt_tally( + tally_result: &EncryptedTally, secret_key: &SecretKey, setup: &DecryptionTallySetup, +) -> Result { + let ge = decrypt(&tally_result.0, secret_key); + + let res = setup + .discrete_log_setup + .discrete_log(ge) + .map_err(|_| DecryptTallyError::CannotDecryptTallyResult)?; + Ok(res) +} diff --git a/rust/catalyst-voting/src/voter.rs b/rust/catalyst-voting/src/voter.rs new file mode 100644 index 00000000000..c67041aa906 --- /dev/null +++ b/rust/catalyst-voting/src/voter.rs @@ -0,0 +1,166 @@ +//! Module containing all primitives related to the voter. + +use rand_core::CryptoRngCore; + +use crate::crypto::{ + elgamal::{encrypt, Ciphertext, PublicKey}, + group::Scalar, +}; + +/// A representation of the voter's voting choice. +/// Represented as a Unit vector which size is `voting_options` +/// and the `choice` value is the index of the unit vector component equals to `1`, +/// and other components equal to `0`. +pub struct Vote { + /// Voter's voting choice. + choice: usize, + /// Number of voting options. + voting_options: usize, +} + +/// A representation of the encrypted vote. +pub struct EncryptedVote(Vec); + +/// A representation of the encryption randomness, used to encrypt the vote. +pub struct EncryptionRandomness(Vec); + +impl EncryptionRandomness { + /// Randomly generate the `EncryptionRandomness`. + pub fn generate(rng: &mut R, voting_options: usize) -> Self { + Self((0..voting_options).map(|_| Scalar::random(rng)).collect()) + } +} + +impl EncryptedVote { + /// Get the ciphertext to the corresponding `voting_option`. + pub(crate) fn get_ciphertext_for_choice(&self, voting_option: usize) -> Option<&Ciphertext> { + self.0.get(voting_option) + } +} + +/// Encrypted vote error +#[derive(thiserror::Error, Debug)] +pub enum VoteError { + /// Incorrect voting choice + #[error( + "Invalid voting choice, the value of choice: {0}, should be less than the number of voting options: {1}." + )] + IncorrectChoiceError(usize, usize), +} + +impl Vote { + /// Generate a vote. + /// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#voting-choice) + /// + /// # Errors + /// - `VoteError` + pub fn new(choice: usize, voting_options: usize) -> Result { + if choice >= voting_options { + return Err(VoteError::IncorrectChoiceError(choice, voting_options)); + } + + Ok(Vote { + choice, + voting_options, + }) + } + + /// Transform the vote into the unit vector. + fn to_unit_vector(&self) -> Vec { + (0..self.voting_options) + .map(|i| { + if i == self.choice { + Scalar::one() + } else { + Scalar::zero() + } + }) + .collect() + } +} + +/// Encrypted vote error +#[derive(thiserror::Error, Debug)] +pub enum EncryptedVoteError { + /// Incorrect randomness length + #[error( + "Invalid randomness length, the length of randomness: {0}, should be equal to the number of voting options: {1}." + )] + IncorrectRandomnessLength(usize, usize), +} + +/// Create a new encrypted vote from the given vote and public key. +/// More detailed described [here](https://input-output-hk.github.io/catalyst-voices/architecture/08_concepts/voting_transaction/crypto/#vote-encryption) +/// +/// # Errors +/// - `EncryptedVoteError` +pub fn encrypt_vote( + vote: &Vote, public_key: &PublicKey, randomness: &EncryptionRandomness, +) -> Result { + if vote.voting_options != randomness.0.len() { + return Err(EncryptedVoteError::IncorrectRandomnessLength( + randomness.0.len(), + vote.voting_options, + )); + } + + let unit_vector = vote.to_unit_vector(); + let ciphers = unit_vector + .iter() + .zip(randomness.0.iter()) + .map(|(m, r)| encrypt(m, public_key, r)) + .collect(); + + Ok(EncryptedVote(ciphers)) +} + +#[cfg(test)] +mod tests { + use proptest::sample::size_range; + use test_strategy::proptest; + + use super::*; + use crate::crypto::elgamal::SecretKey; + + #[test] + fn vote_test() { + let voting_options = 3; + + let vote = Vote::new(0, voting_options).unwrap(); + assert_eq!(vote.to_unit_vector(), vec![ + Scalar::one(), + Scalar::zero(), + Scalar::zero() + ]); + + let vote = Vote::new(1, voting_options).unwrap(); + assert_eq!(vote.to_unit_vector(), vec![ + Scalar::zero(), + Scalar::one(), + Scalar::zero() + ]); + + let vote = Vote::new(2, voting_options).unwrap(); + assert_eq!(vote.to_unit_vector(), vec![ + Scalar::zero(), + Scalar::zero(), + Scalar::one() + ]); + + assert!(Vote::new(3, voting_options).is_err()); + assert!(Vote::new(4, voting_options).is_err()); + } + + #[proptest] + fn encrypt_test( + secret_key: SecretKey, #[strategy(1..10usize)] voting_options: usize, + #[any(size_range(#voting_options).lift())] randomness: Vec, + ) { + let public_key = secret_key.public_key(); + let vote = Vote::new(0, voting_options).unwrap(); + + let encrypted = + encrypt_vote(&vote, &public_key, &EncryptionRandomness(randomness)).unwrap(); + assert_eq!(encrypted.0.len(), vote.voting_options); + } +} diff --git a/rust/catalyst-voting/tests/voting_test.rs b/rust/catalyst-voting/tests/voting_test.rs new file mode 100644 index 00000000000..d68ab42e43b --- /dev/null +++ b/rust/catalyst-voting/tests/voting_test.rs @@ -0,0 +1,69 @@ +//! A general voting integration test, which performs a full voting procedure. + +use catalyst_voting::{ + decrypt_tally, encrypt_vote, tally, DecryptionTallySetup, EncryptionRandomness, SecretKey, Vote, +}; +use proptest::prelude::ProptestConfig; +use test_strategy::{proptest, Arbitrary}; + +const VOTING_OPTIONS: usize = 3; + +#[derive(Arbitrary, Debug)] +struct Voter { + voting_power: u32, + // range from 0 to `VOTING_OPTIONS` + #[strategy(0..3_usize)] + choice: usize, +} + +#[proptest(ProptestConfig::with_cases(1))] +fn voting_test(voters: [Voter; 100]) { + let mut rng = rand_core::OsRng; + + let election_secret_key = SecretKey::generate(&mut rng); + let election_public_key = election_secret_key.public_key(); + + let votes: Vec<_> = voters + .iter() + .map(|voter| Vote::new(voter.choice, VOTING_OPTIONS).unwrap()) + .collect(); + + let voters_randomness: Vec<_> = (0..voters.len()) + .map(|_| EncryptionRandomness::generate(&mut rng, VOTING_OPTIONS)) + .collect(); + + let encrypted_votes: Vec<_> = votes + .iter() + .zip(voters_randomness.iter()) + .map(|(vote, r)| encrypt_vote(vote, &election_public_key, r).unwrap()) + .collect(); + + let voting_powers: Vec<_> = voters + .iter() + .map(|voter| u64::from(voter.voting_power)) + .collect(); + + let encrypted_tallies: Vec<_> = (0..VOTING_OPTIONS) + .map(|voting_option| tally(voting_option, &encrypted_votes, &voting_powers).unwrap()) + .collect(); + + let total_voting_power = voting_powers.iter().sum(); + let decryption_tally_setup = DecryptionTallySetup::new(total_voting_power).unwrap(); + + let decrypted_tallies: Vec<_> = encrypted_tallies + .iter() + .map(|t| decrypt_tally(t, &election_secret_key, &decryption_tally_setup).unwrap()) + .collect(); + + let expected_tallies: Vec<_> = (0..VOTING_OPTIONS) + .map(|i| { + voters + .iter() + .filter(|v| v.choice == i) + .map(|v| u64::from(v.voting_power)) + .sum::() + }) + .collect(); + + assert_eq!(decrypted_tallies, expected_tallies); +}