diff --git a/chain-impl-mockchain/src/certificate/test.rs b/chain-impl-mockchain/src/certificate/test.rs index a14092da9..33d6594e7 100644 --- a/chain-impl-mockchain/src/certificate/test.rs +++ b/chain-impl-mockchain/src/certificate/test.rs @@ -181,7 +181,7 @@ impl Arbitrary for VotePlan { let mut keys = Vec::new(); // it should have been 256 but is limited for the sake of adequate test times - let keys_n = g.next_u32() % 16; + let keys_n = g.next_u32() % 15 + 1; let mut seed = [0u8; 32]; g.fill_bytes(&mut seed); let mut rng = rand_chacha::ChaCha20Rng::from_seed(seed); diff --git a/chain-impl-mockchain/src/testing/gen/vote.rs b/chain-impl-mockchain/src/testing/gen/vote.rs index dfffbf9c7..5bb4c6d30 100644 --- a/chain-impl-mockchain/src/testing/gen/vote.rs +++ b/chain-impl-mockchain/src/testing/gen/vote.rs @@ -10,7 +10,7 @@ use crate::{ }; use chain_core::property::BlockDate as BlockDateProp; use chain_crypto::digest::DigestOf; -use chain_vote::Crs; +use chain_vote::{Crs, ElectionPublicKey, Vote}; use rand_core::{CryptoRng, RngCore}; use typed_bytes::ByteBuilder; @@ -105,13 +105,13 @@ impl VoteTestGen { rng: &mut R, ) -> Payload { let encrypting_key = - chain_vote::ElectionPublicKey::from_participants(vote_plan.committee_public_keys()); + ElectionPublicKey::from_participants(vote_plan.committee_public_keys()); let crs = Crs::from_hash(&vote_plan.to_id().as_ref()); let (encrypted_vote, proof) = encrypting_key.encrypt_and_prove_vote( rng, &crs, - chain_vote::Vote::new( + Vote::new( proposal.options().choice_range().clone().max().unwrap() as usize + 1, choice.as_byte() as usize, ), diff --git a/chain-impl-mockchain/src/vote/manager.rs b/chain-impl-mockchain/src/vote/manager.rs index 664b46b3c..5c3c411dc 100644 --- a/chain-impl-mockchain/src/vote/manager.rs +++ b/chain-impl-mockchain/src/vote/manager.rs @@ -1,6 +1,6 @@ use crate::{ certificate::DecryptedPrivateTallyProposal, - vote::{Choice, Payload, TallyError}, + vote::{Choice, Payload, PayloadType, TallyError}, }; use crate::{ certificate::{DecryptedPrivateTally, Proposal, VoteAction, VoteCast, VotePlan, VotePlanId}, @@ -11,7 +11,7 @@ use crate::{ transaction::UnspecifiedAccountIdentifier, vote::{self, CommitteeId, Options, Tally, TallyResult, VotePlanStatus, VoteProposalStatus}, }; -use chain_vote::{committee, Crs, ElectionPublicKey, EncryptedTally}; +use chain_vote::{committee, Ballot, Crs, ElectionPublicKey, EncryptedTally}; use imhamt::Hamt; use thiserror::Error; @@ -29,16 +29,36 @@ pub struct VotePlanManager { id: VotePlanId, plan: Arc, committee: Arc>, - proposal_managers: ProposalManagers, } +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum ValidatedPayload { + Public(Choice), + Private(Ballot), +} + +#[derive(Clone, Eq, PartialEq, Debug)] +struct ValidatedVoteCast { + payload: ValidatedPayload, + proposal_index: usize, +} + #[derive(Clone, PartialEq, Eq)] -struct ProposalManagers(Vec); +enum ProposalManagers { + Public { + managers: Vec, + }, + Private { + managers: Vec, + crs: Arc, + election_pk: Arc, + }, +} #[derive(Clone, PartialEq, Eq)] struct ProposalManager { - votes_by_voters: Hamt, + votes_by_voters: Hamt, options: Options, tally: Option, action: VoteAction, @@ -67,8 +87,8 @@ pub enum VoteError { #[error("{received:?} is not the expected payload type, expected {expected:?}")] InvalidPayloadType { - received: vote::PayloadType, - expected: vote::PayloadType, + received: PayloadType, + expected: PayloadType, }, #[error("It is not possible to tally the votes for the proposals, time to tally the votes is between {start} to {end}.")] @@ -84,7 +104,7 @@ pub enum VoteError { }, #[error("Invalid private vote verification")] - VoteVerificationError, + VoteVerificationError(#[from] chain_vote::BallotVerificationError), #[error("Invalid private vote size (expected {expected}, got {actual})")] PrivateVoteInvalidSize { actual: usize, expected: usize }, @@ -119,10 +139,8 @@ impl ProposalManager { pub fn vote( &self, identifier: UnspecifiedAccountIdentifier, - cast: VoteCast, + payload: ValidatedPayload, ) -> Result { - let payload = cast.into_payload(); - // we don't mind if we are replacing a vote let votes_by_voters = self.votes_by_voters @@ -135,12 +153,35 @@ impl ProposalManager { }) } - pub fn validate_vote(&self, cast: &VoteCast) -> Result<(), VoteError> { - let payload = cast.payload(); + pub fn validate_public_vote(&self, cast: VoteCast) -> Result { + let payload = cast.into_payload(); match payload { - Payload::Public { .. } => Ok(()), - Payload::Private { encrypted_vote, .. } => { + Payload::Public { choice } => Ok(ValidatedPayload::Public(choice)), + Payload::Private { .. } => Err(VoteError::InvalidPayloadType { + received: PayloadType::Private, + expected: PayloadType::Public, + }), + } + } + + pub fn validate_private_vote( + &self, + cast: VoteCast, + crs: &Crs, + election_pk: &ElectionPublicKey, + ) -> Result { + let payload = cast.into_payload(); + + match payload { + Payload::Public { .. } => Err(VoteError::InvalidPayloadType { + received: PayloadType::Public, + expected: PayloadType::Private, + }), + Payload::Private { + encrypted_vote, + proof, + } => { let actual_size = encrypted_vote.as_inner().len(); let expected_size = self.options.choice_range().len(); if actual_size != expected_size { @@ -149,7 +190,12 @@ impl ProposalManager { actual: actual_size, }) } else { - Ok(()) + Ok(ValidatedPayload::Private(Ballot::try_from_vote_and_proof( + encrypted_vote.as_inner().clone(), + proof.as_inner(), + &crs, + &election_pk, + )?)) } } } @@ -171,13 +217,13 @@ impl ProposalManager { if let Some(account_id) = id.to_single_account() { if let Some(stake) = stake.by(&account_id) { match payload { - vote::Payload::Public { choice } => { + ValidatedPayload::Public(choice) => { results.add_vote(*choice, stake)?; } - vote::Payload::Private { .. } => { + ValidatedPayload::Private(_) => { return Err(VoteError::InvalidPayloadType { - expected: vote::PayloadType::Public, - received: vote::PayloadType::Private, + expected: PayloadType::Public, + received: PayloadType::Private, }); } } @@ -198,7 +244,12 @@ impl ProposalManager { } #[must_use = "Compute the PrivateTally in a new ProposalManager, does not modify self"] - pub fn private_tally(&self, stake: &StakeControl) -> Result { + pub fn private_tally( + &self, + stake: &StakeControl, + election_pk: &ElectionPublicKey, + crs: &Crs, + ) -> Result { use rayon::prelude::*; let tally_size = self.options.choice_range().clone().max().unwrap() as usize + 1; @@ -211,31 +262,33 @@ impl ProposalManager { if let Some(account_id) = id.to_single_account() { if let Some(stake) = stake.by(&account_id) { match payload { - vote::Payload::Public { .. } => { + ValidatedPayload::Public(_) => { return Some(Err(VoteError::InvalidPayloadType { - expected: vote::PayloadType::Private, - received: vote::PayloadType::Public, + expected: PayloadType::Private, + received: PayloadType::Public, })) } - vote::Payload::Private { - encrypted_vote, - proof: _, - } => return Some(Ok((encrypted_vote.as_inner(), stake.0))), + ValidatedPayload::Private(ballot) => { + return Some(Ok((ballot, stake.0))) + } } } } None }) .try_fold_with( - EncryptedTally::new(tally_size), + EncryptedTally::new(tally_size, election_pk.clone(), crs.clone()), |mut tally, vote_with_stake| { - vote_with_stake.map(|(encrypted_vote, stake)| { - tally.add(encrypted_vote, stake); + vote_with_stake.map(|(ballot, stake)| { + tally.add(&ballot, stake); tally }) }, ) - .try_reduce(|| EncryptedTally::new(tally_size), |a, b| Ok(a + b))?; + .try_reduce( + || EncryptedTally::new(tally_size, election_pk.clone(), crs.clone()), + |a, b| Ok(a + b), + )?; Ok(Self { votes_by_voters: self.votes_by_voters.clone(), @@ -372,13 +425,41 @@ impl ProposalManager { impl ProposalManagers { fn new(plan: &VotePlan) -> Self { - let proposal_managers = plan + let managers = plan .proposals() .iter() .map(|proposal| ProposalManager::new(proposal)) .collect(); + match plan.payload_type() { + PayloadType::Public => Self::Public { managers }, + PayloadType::Private => { + let crs = Arc::new(Crs::from_hash(plan.to_id().as_ref())); + let election_pk = Arc::new(ElectionPublicKey::from_participants( + plan.committee_public_keys(), + )); + + Self::Private { + managers, + crs, + election_pk, + } + } + } + } + + fn managers(&self) -> &[ProposalManager] { + match self { + Self::Public { managers } | Self::Private { managers, .. } => &managers, + } + } - Self(proposal_managers) + fn managers_mut(&mut self) -> &mut [ProposalManager] { + match self { + Self::Public { ref mut managers } + | Self::Private { + ref mut managers, .. + } => managers, + } } /// attempt to apply the vote to one of the proposals @@ -390,28 +471,22 @@ impl ProposalManagers { pub fn vote( &self, identifier: UnspecifiedAccountIdentifier, - cast: VoteCast, + vote_cast: ValidatedVoteCast, ) -> Result { - let proposal_index = cast.proposal_index() as usize; - if let Some(manager) = self.0.get(proposal_index) { - let updated_manager = manager.vote(identifier, cast)?; - + let proposal_index = vote_cast.proposal_index; + if let Some(manager) = self.managers().get(proposal_index) { + let updated_manager = manager.vote(identifier, vote_cast.payload)?; // only clone the array if it does make sens to do so: // // * the index exist // * updated_manager succeed let mut updated = self.clone(); - // not unsafe to call this function since we already know this // `proposal_index` already exist in the array - unsafe { *updated.0.get_unchecked_mut(proposal_index) = updated_manager }; - + unsafe { *updated.managers_mut().get_unchecked_mut(proposal_index) = updated_manager }; Ok(updated) } else { - Err(VoteError::InvalidVoteProposal { - num_proposals: self.0.len(), - vote: cast, - }) + unreachable!("the vote has been already validated"); } } @@ -424,36 +499,78 @@ impl ProposalManagers { where F: FnMut(&VoteAction), { - let mut proposals = Vec::with_capacity(self.0.len()); - for proposal in self.0.iter() { - proposals.push(proposal.public_tally(stake, governance, &mut f)?); + match self { + Self::Public { managers } => { + let mut proposals = Vec::with_capacity(managers.len()); + for proposal in managers.iter() { + proposals.push(proposal.public_tally(stake, governance, &mut f)?); + } + Ok(Self::Public { + managers: proposals, + }) + } + _ => Err(VoteError::InvalidPayloadType { + expected: PayloadType::Public, + received: PayloadType::Private, + }), } - - Ok(Self(proposals)) } /// validate the vote against the proposal: verify that the proposal exists /// and the the length of the ciphertext is correct (if applicable) - pub fn validate_vote(&self, cast: &VoteCast) -> Result<(), VoteError> { + pub fn validate_vote(&self, cast: VoteCast) -> Result { let proposal_index = cast.proposal_index() as usize; - if let Some(manager) = self.0.get(proposal_index) { - manager.validate_vote(cast) - } else { - Err(VoteError::InvalidVoteProposal { - num_proposals: self.0.len(), - vote: cast.clone(), - }) - } + let payload = match self { + Self::Public { managers } => managers + .get(proposal_index) + .ok_or(VoteError::InvalidVoteProposal { + num_proposals: managers.len(), + vote: cast.clone(), + })? + .validate_public_vote(cast), + Self::Private { + managers, + crs, + election_pk, + } => managers + .get(proposal_index) + .ok_or(VoteError::InvalidVoteProposal { + num_proposals: managers.len(), + vote: cast.clone(), + })? + .validate_private_vote(cast, &crs, &election_pk), + }?; + + Ok(ValidatedVoteCast { + payload, + proposal_index, + }) } pub fn start_private_tally(&self, stake: &StakeControl) -> Result { use rayon::prelude::*; - let proposals = self - .0 - .par_iter() - .map(|proposal| proposal.private_tally(stake)) - .collect::>()?; - Ok(Self(proposals)) + + match self { + Self::Private { + managers, + crs, + election_pk, + } => { + let proposals = managers + .par_iter() + .map(|proposal| proposal.private_tally(stake, &election_pk, &crs)) + .collect::>()?; + Ok(Self::Private { + managers: proposals, + crs: crs.clone(), + election_pk: election_pk.clone(), + }) + } + _ => Err(VoteError::InvalidPayloadType { + received: PayloadType::Public, + expected: PayloadType::Private, + }), + } } pub fn finalize_private_tally( @@ -466,16 +583,34 @@ impl ProposalManagers { where F: FnMut(&VoteAction), { - let mut proposals = Vec::with_capacity(self.0.len()); - for (proposal_manager, decrypted_proposal) in self.0.iter().zip(decrypted_tally.iter()) { - proposals.push(proposal_manager.finalize_private_tally( - committee_pks, - decrypted_proposal, - governance, - &mut f, - )?); + match self { + Self::Private { + managers, + crs, + election_pk, + } => { + let mut proposals = Vec::with_capacity(managers.len()); + for (proposal_manager, decrypted_proposal) in + managers.iter().zip(decrypted_tally.iter()) + { + proposals.push(proposal_manager.finalize_private_tally( + committee_pks, + decrypted_proposal, + governance, + &mut f, + )?); + } + Ok(Self::Private { + managers: proposals, + crs: crs.clone(), + election_pk: election_pk.clone(), + }) + } + _ => Err(VoteError::InvalidPayloadType { + received: PayloadType::Public, + expected: PayloadType::Private, + }), } - Ok(Self(proposals)) } } @@ -505,7 +640,7 @@ impl VotePlanManager { .plan() .proposals() .iter() - .zip(self.proposal_managers.0.iter()) + .zip(self.proposal_managers.managers().iter()) .enumerate() .map(|(index, (proposal, manager))| VoteProposalStatus { index: index as u8, @@ -572,50 +707,36 @@ impl VotePlanManager { cast: VoteCast, ) -> Result { if cast.vote_plan() != self.id() { - Err(VoteError::InvalidVotePlan { + return Err(VoteError::InvalidVotePlan { expected: self.id().clone(), vote: cast, - }) - } else if !self.can_vote(block_date) { - Err(VoteError::NotVoteTime { + }); + } + + if !self.can_vote(block_date) { + return Err(VoteError::NotVoteTime { start: self.plan().vote_start(), end: self.plan().vote_end(), vote: cast, - }) - } else if self.plan().payload_type() != cast.payload().payload_type() { - Err(VoteError::InvalidPayloadType { + }); + } + if self.plan().payload_type() != cast.payload().payload_type() { + return Err(VoteError::InvalidPayloadType { expected: self.plan().payload_type(), received: cast.payload().payload_type(), - }) - // verify vote if private - } else if let Err(e) = match &cast.payload() { - Payload::Public { .. } => Ok(()), - Payload::Private { - encrypted_vote, - proof, - } => { - let crs = Crs::from_hash(&self.plan.as_ref().to_id().as_ref()); - let ciphertext = encrypted_vote.as_inner(); - self.proposal_managers.validate_vote(&cast)?; - let pk = ElectionPublicKey::from_participants(self.plan.committee_public_keys()); - if !proof.as_inner().verify(&crs, &pk.as_raw(), ciphertext) { - Err(VoteError::VoteVerificationError) - } else { - Ok(()) - } - } - } { - Err(e) - } else { - let proposal_managers = self.proposal_managers.vote(identifier, cast)?; - - Ok(Self { - proposal_managers, - plan: Arc::clone(&self.plan), - id: self.id.clone(), - committee: Arc::clone(&self.committee), - }) + }); } + + let vote = self.proposal_managers.validate_vote(cast)?; + + let proposal_managers = self.proposal_managers.vote(identifier, vote)?; + + Ok(Self { + proposal_managers, + plan: Arc::clone(&self.plan), + id: self.id.clone(), + committee: Arc::clone(&self.committee), + }) } pub fn public_tally( @@ -640,7 +761,7 @@ impl VotePlanManager { return Err(VoteError::InvalidTallyCommittee); } - if self.plan.payload_type() != vote::PayloadType::Public { + if self.plan.payload_type() != PayloadType::Public { return Err(TallyError::InvalidPrivacy.into()); } @@ -671,7 +792,7 @@ impl VotePlanManager { return Err(VoteError::InvalidTallyCommittee); } - if self.plan.payload_type() != vote::PayloadType::Private { + if self.plan.payload_type() != PayloadType::Private { return Err(TallyError::InvalidPrivacy.into()); } @@ -724,36 +845,49 @@ mod tests { #[test] pub fn proposal_manager_insert_vote() { let vote_plan = VoteTestGen::vote_plan(); - let vote_cast_payload = vote::Payload::public(vote::Choice::new(1)); + let vote_choice = vote::Choice::new(1); + let vote_cast_payload = vote::Payload::public(vote_choice); let vote_cast = VoteCast::new(vote_plan.to_id(), 0, vote_cast_payload.clone()); let mut proposal_manager = ProposalManager::new(vote_plan.proposals().get(0).unwrap()); + let vote = proposal_manager.validate_public_vote(vote_cast).unwrap(); + let identifier = TestGen::unspecified_account_identifier(); - proposal_manager = proposal_manager - .vote(identifier.clone(), vote_cast) - .unwrap(); + proposal_manager = proposal_manager.vote(identifier.clone(), vote).unwrap(); let (_, actual_vote_cast_payload) = proposal_manager .votes_by_voters .iter() .find(|(x, _y)| **x == identifier) .unwrap(); - assert_eq!(*actual_vote_cast_payload, vote_cast_payload); + assert_eq!( + *actual_vote_cast_payload, + ValidatedPayload::Public(vote_choice) + ); } #[test] pub fn proposal_manager_replace_vote() { let vote_plan = VoteTestGen::vote_plan(); - let first_vote_cast_payload = VoteTestGen::vote_cast_payload(); - let second_vote_cast_payload = VoteTestGen::vote_cast_payload(); - - let first_vote_cast = VoteCast::new(vote_plan.to_id(), 0, first_vote_cast_payload); - let second_vote_cast = - VoteCast::new(vote_plan.to_id(), 0, second_vote_cast_payload.clone()); + let first_vote_choice = vote::Choice::new(1); + let second_vote_choice = vote::Choice::new(2); + let first_vote_cast_payload = vote::Payload::public(first_vote_choice); + let second_vote_cast_payload = vote::Payload::public(second_vote_choice); let mut proposal_manager = ProposalManager::new(vote_plan.proposals().get(0).unwrap()); + let first_vote_cast = proposal_manager + .validate_public_vote(VoteCast::new(vote_plan.to_id(), 0, first_vote_cast_payload)) + .unwrap(); + let second_vote_cast = proposal_manager + .validate_public_vote(VoteCast::new( + vote_plan.to_id(), + 0, + second_vote_cast_payload.clone(), + )) + .unwrap(); + let identifier = TestGen::unspecified_account_identifier(); proposal_manager = proposal_manager .vote(identifier.clone(), first_vote_cast) @@ -767,7 +901,10 @@ mod tests { .iter() .find(|(x, _y)| **x == identifier) .unwrap(); - assert_eq!(*actual_vote_cast_payload, second_vote_cast_payload); + assert_eq!( + *actual_vote_cast_payload, + ValidatedPayload::Public(second_vote_choice) + ); } const CENT: NonZeroU64 = unsafe { NonZeroU64::new_unchecked(100) }; @@ -786,7 +923,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), proposals, - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); @@ -828,7 +965,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), proposals, - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); @@ -897,7 +1034,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), proposals, - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); @@ -951,7 +1088,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), proposals, - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); @@ -1028,7 +1165,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), proposals, - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); @@ -1038,23 +1175,28 @@ mod tests { ProposalManager::new(vote_plan.proposals().get(1).unwrap()); let identifier = TestGen::unspecified_account_identifier(); + let proposals = ProposalManagers::new(&vote_plan); - let first_vote_cast = VoteCast::new( - vote_plan.to_id(), - 0, - VoteTestGen::vote_cast_payload_for(&favorable), - ); + let first_vote_cast = proposals + .validate_vote(VoteCast::new( + vote_plan.to_id(), + 0, + VoteTestGen::vote_cast_payload_for(&favorable), + )) + .unwrap(); first_proposal_manager = first_proposal_manager - .vote(identifier.clone(), first_vote_cast.clone()) + .vote(identifier.clone(), first_vote_cast.payload.clone()) .unwrap(); - let second_vote_cast = VoteCast::new( - vote_plan.to_id(), - 1, - VoteTestGen::vote_cast_payload_for(&favorable), - ); + let second_vote_cast = proposals + .validate_vote(VoteCast::new( + vote_plan.to_id(), + 1, + VoteTestGen::vote_cast_payload_for(&favorable), + )) + .unwrap(); second_proposal_manager = second_proposal_manager - .vote(identifier.clone(), second_vote_cast.clone()) + .vote(identifier.clone(), second_vote_cast.payload.clone()) .unwrap(); let mut stake_controlled = StakeControl::new(); @@ -1062,7 +1204,6 @@ mod tests { stake_controlled.add_to(identifier.to_single_account().unwrap(), Stake(51)); stake_controlled = stake_controlled.add_unassigned(Stake(49)); - let proposals = ProposalManagers::new(&vote_plan); let _ = proposals.vote(identifier.clone(), first_vote_cast); let _ = proposals.vote(identifier, second_vote_cast); @@ -1130,8 +1271,9 @@ mod tests { #[test] pub fn proposal_managers_many_votes() { let vote_plan = VoteTestGen::vote_plan_with_proposals(2); - let first_vote_cast_payload = VoteTestGen::vote_cast_payload(); - let second_vote_cast_payload = VoteTestGen::vote_cast_payload(); + let choice = Choice::new(1); + let first_vote_cast_payload = VoteTestGen::vote_cast_payload_for(&choice); + let second_vote_cast_payload = VoteTestGen::vote_cast_payload_for(&choice); let first_vote_cast = VoteCast::new(vote_plan.to_id(), 0, first_vote_cast_payload.clone()); let second_vote_cast = @@ -1139,58 +1281,80 @@ mod tests { let mut proposal_managers = ProposalManagers::new(&vote_plan); + let first_vote_cast_validated = proposal_managers.validate_vote(first_vote_cast).unwrap(); + let second_vote_cast_validated = proposal_managers + .validate_vote(second_vote_cast.clone()) + .unwrap(); + let identifier = TestGen::unspecified_account_identifier(); proposal_managers = proposal_managers - .vote(identifier.clone(), first_vote_cast) + .vote(identifier.clone(), first_vote_cast_validated) .unwrap(); proposal_managers = proposal_managers - .vote(identifier.clone(), second_vote_cast) + .vote(identifier.clone(), second_vote_cast_validated) .unwrap(); let (_, actual_vote_cast_payload) = proposal_managers - .0 + .managers() .get(0) .unwrap() .votes_by_voters .iter() .find(|(x, _y)| **x == identifier) .unwrap(); - assert_eq!(*actual_vote_cast_payload, first_vote_cast_payload); + assert_eq!( + *actual_vote_cast_payload, + ValidatedPayload::Public(choice.clone()) + ); let (_, actual_vote_cast_payload) = proposal_managers - .0 + .managers() .get(1) .unwrap() .votes_by_voters .iter() .find(|(x, _y)| **x == identifier) .unwrap(); - assert_eq!(*actual_vote_cast_payload, second_vote_cast_payload); + assert_eq!( + *actual_vote_cast_payload, + ValidatedPayload::Public(choice.clone()) + ); } #[test] pub fn vote_for_nonexisting_proposal() { let vote_plan = VoteTestGen::vote_plan_with_proposals(1); - let vote_cast = VoteCast::new(vote_plan.to_id(), 2, VoteTestGen::vote_cast_payload()); - let proposal_managers = ProposalManagers::new(&vote_plan); assert!(proposal_managers - .vote(TestGen::unspecified_account_identifier(), vote_cast) + .validate_vote(VoteCast::new( + vote_plan.to_id(), + 2, + VoteTestGen::vote_cast_payload() + ),) .is_err()); } #[test] pub fn proposal_managers_update_vote() { let vote_plan = VoteTestGen::vote_plan_with_proposals(2); - let first_vote_cast_payload = VoteTestGen::vote_cast_payload(); - let second_vote_cast_payload = VoteTestGen::vote_cast_payload(); - - let first_vote_cast = VoteCast::new(vote_plan.to_id(), 0, first_vote_cast_payload); - let second_vote_cast = - VoteCast::new(vote_plan.to_id(), 0, second_vote_cast_payload.clone()); + let first_choice = Choice::new(0); + let second_choice = Choice::new(1); + let first_vote_cast_payload = VoteTestGen::vote_cast_payload_for(&first_choice); + let second_vote_cast_payload = VoteTestGen::vote_cast_payload_for(&second_choice); let mut proposal_managers = ProposalManagers::new(&vote_plan); + let first_vote_cast = proposal_managers + .validate_vote(VoteCast::new(vote_plan.to_id(), 0, first_vote_cast_payload)) + .unwrap(); + let second_vote_cast = proposal_managers + .validate_vote(VoteCast::new( + vote_plan.to_id(), + 0, + second_vote_cast_payload.clone(), + )) + .unwrap(); + let identifier = TestGen::unspecified_account_identifier(); proposal_managers = proposal_managers .vote(identifier.clone(), first_vote_cast) @@ -1200,14 +1364,17 @@ mod tests { .unwrap(); let (_, actual_vote_cast_payload) = proposal_managers - .0 + .managers() .get(0) .unwrap() .votes_by_voters .iter() .find(|(x, _y)| **x == identifier) .unwrap(); - assert_eq!(*actual_vote_cast_payload, second_vote_cast_payload); + assert_eq!( + *actual_vote_cast_payload, + ValidatedPayload::Public(second_choice) + ); } #[quickcheck] @@ -1302,7 +1469,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), VoteTestGen::proposals(3), - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); @@ -1333,7 +1500,7 @@ mod tests { BlockDate::from_epoch_slot_id(2, 0), BlockDate::from_epoch_slot_id(3, 0), VoteTestGen::proposals(3), - vote::PayloadType::Public, + PayloadType::Public, Vec::new(), ); diff --git a/chain-impl-mockchain/src/vote/mod.rs b/chain-impl-mockchain/src/vote/mod.rs index 233d2e50d..9a1141caa 100644 --- a/chain-impl-mockchain/src/vote/mod.rs +++ b/chain-impl-mockchain/src/vote/mod.rs @@ -16,7 +16,7 @@ pub use self::{ choice::{Choice, Options}, committee::CommitteeId, ledger::{VotePlanLedger, VotePlanLedgerError}, - manager::{VoteError, VotePlanManager}, + manager::{ValidatedPayload, VoteError, VotePlanManager}, payload::{EncryptedVote, Payload, PayloadType, ProofOfCorrectVote, TryFromIntError}, privacy::encrypt_vote, status::{VotePlanStatus, VoteProposalStatus}, diff --git a/chain-impl-mockchain/src/vote/payload.rs b/chain-impl-mockchain/src/vote/payload.rs index 6418ac02a..a7a08d545 100644 --- a/chain-impl-mockchain/src/vote/payload.rs +++ b/chain-impl-mockchain/src/vote/payload.rs @@ -190,6 +190,7 @@ impl Default for PayloadType { #[cfg(any(test, feature = "property-test-api"))] mod tests { use super::*; + use chain_vote::{Crs, ElectionPublicKey}; use quickcheck::{Arbitrary, Gen}; impl Arbitrary for PayloadType { @@ -204,7 +205,7 @@ mod tests { impl Arbitrary for Payload { fn arbitrary(g: &mut G) -> Self { - use chain_vote::{Crs, ElectionPublicKey, MemberCommunicationKey, MemberState, Vote}; + use chain_vote::{MemberCommunicationKey, MemberState, Vote}; use rand_core::SeedableRng; match PayloadType::arbitrary(g) { diff --git a/chain-impl-mockchain/src/vote/status.rs b/chain-impl-mockchain/src/vote/status.rs index 4c3672085..a58d875fa 100644 --- a/chain-impl-mockchain/src/vote/status.rs +++ b/chain-impl-mockchain/src/vote/status.rs @@ -2,7 +2,7 @@ use crate::{ certificate::{ExternalProposalId, VotePlanId}, date::BlockDate, transaction::UnspecifiedAccountIdentifier, - vote::{Options, Payload, PayloadType, Tally}, + vote::{Options, PayloadType, Tally, ValidatedPayload}, }; use chain_vote::MemberPublicKey; use imhamt::Hamt; @@ -23,5 +23,5 @@ pub struct VoteProposalStatus { pub proposal_id: ExternalProposalId, pub options: Options, pub tally: Option, - pub votes: Hamt, + pub votes: Hamt, } diff --git a/chain-vote/src/encrypted_vote.rs b/chain-vote/src/encrypted_vote.rs index 85b36eeb0..76fa6192e 100644 --- a/chain-vote/src/encrypted_vote.rs +++ b/chain-vote/src/encrypted_vote.rs @@ -1,4 +1,6 @@ use crate::cryptography::{Ciphertext, UnitVectorZkp}; +use crate::tally::ElectionFingerprint; +use crate::{Crs, ElectionPublicKey}; use chain_crypto::ec::Scalar; /// A vote is represented by a standard basis unit vector of an N dimensional space /// @@ -17,6 +19,47 @@ pub type EncryptedVote = Vec; /// the `EncryptedVote` is indeed a unit vector, and contains a vote for a single candidate. pub type ProofOfCorrectVote = UnitVectorZkp; +/// Submitted ballot, which contains an always verified vote. +/// Used for early verification of a vote without requiring additional +/// checks down the chain. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Ballot { + vote: EncryptedVote, + // Used to verify that the ballot is applied to the correct + // encrypted tally + fingerprint: ElectionFingerprint, +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +#[error("Invalid vote proof")] +pub struct BallotVerificationError; + +impl Ballot { + pub fn try_from_vote_and_proof( + vote: EncryptedVote, + proof: &ProofOfCorrectVote, + crs: &Crs, + pk: &ElectionPublicKey, + ) -> Result { + if !proof.verify(crs, &pk.0, &vote) { + return Err(BallotVerificationError); + } + + Ok(Self { + vote, + fingerprint: (pk, crs).into(), + }) + } + + pub fn vote(&self) -> &EncryptedVote { + &self.vote + } + + pub(super) fn fingerprint(&self) -> &ElectionFingerprint { + &self.fingerprint + } +} + /// To achieve logarithmic communication complexity in the unit_vector ZKP, we represent /// votes as Power of Two Padded vector structures. #[derive(Clone)] diff --git a/chain-vote/src/lib.rs b/chain-vote/src/lib.rs index 2e5b181d8..9f30a585f 100644 --- a/chain-vote/src/lib.rs +++ b/chain-vote/src/lib.rs @@ -19,6 +19,6 @@ pub use chain_crypto::ec::BabyStepsTable as TallyOptimizationTable; pub use crate::{ committee::{ElectionPublicKey, MemberCommunicationKey, MemberPublicKey, MemberState}, cryptography::Ciphertext, //todo: why this? - encrypted_vote::{EncryptedVote, ProofOfCorrectVote, Vote}, + encrypted_vote::{Ballot, BallotVerificationError, EncryptedVote, ProofOfCorrectVote, Vote}, tally::{Crs, EncryptedTally, Tally, TallyDecryptShare}, }; diff --git a/chain-vote/src/tally.rs b/chain-vote/src/tally.rs index 439dc9ddd..cbee156ad 100644 --- a/chain-vote/src/tally.rs +++ b/chain-vote/src/tally.rs @@ -1,13 +1,16 @@ use crate::{ committee::*, cryptography::{Ciphertext, CorrectElGamalDecrZkp}, - encrypted_vote::EncryptedVote, + encrypted_vote::Ballot, }; use chain_crypto::ec::{ baby_step_giant_step, BabyStepsTable as TallyOptimizationTable, GroupElement, }; +use cryptoxide::blake2b::Blake2b; +use cryptoxide::digest::Digest; use rand_core::{CryptoRng, RngCore}; +use std::convert::TryInto; /// Secret key for opening vote pub type OpeningVoteKey = MemberSecretKey; @@ -21,11 +24,36 @@ pub type ProofOfCorrectShare = CorrectElGamalDecrZkp; /// Common Reference String pub type Crs = GroupElement; -/// `EncryptedTally` is formed by one ciphertext per existing option, the `election_pk`, and the -/// `crs`. +/// An encrypted vote is only valid for specific values of the election public key and crs. +/// It may be useful to check early if a vote is valid before actually adding it to the tally, +/// and it is therefore important to verify that it is later added to an encrypted tally that +/// is consistent with the election public key and crs it was verified against. +/// To reduce memory occupation, we use a hash of those two values. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) struct ElectionFingerprint([u8; ElectionFingerprint::BYTES_LEN]); + +impl ElectionFingerprint { + const BYTES_LEN: usize = 32; +} + +impl From<(&ElectionPublicKey, &Crs)> for ElectionFingerprint { + fn from(from: (&ElectionPublicKey, &Crs)) -> Self { + let (election_pk, crs) = from; + let mut hasher = Blake2b::new(32); + hasher.input(&crs.to_bytes()); + hasher.input(&election_pk.to_bytes()); + let mut fingerprint = [0; 32]; + hasher.result(&mut fingerprint); + Self(fingerprint) + } +} + +/// `EncryptedTally` is formed by one ciphertext per existing option and a fingerprint that +/// identifies the election parameters used (crs and election public key) #[derive(Clone, Debug, PartialEq, Eq)] pub struct EncryptedTally { r: Vec, + fingerprint: ElectionFingerprint, } /// `TallyDecryptShare` contains one decryption share per existing option. All committee @@ -79,20 +107,26 @@ impl EncryptedTally { /// Initialise a new tally with N different options. The `EncryptedTally` is computed using /// the additive homomorphic property of the elgamal `Ciphertext`s, and is therefore initialised /// with zero ciphertexts. - pub fn new(options: usize) -> Self { + pub fn new(options: usize, election_pk: ElectionPublicKey, crs: Crs) -> Self { let r = vec![Ciphertext::zero(); options]; - EncryptedTally { r } + EncryptedTally { + r, + fingerprint: (&election_pk, &crs).into(), + } } - /// Add a submitted `ballot`, with a specific `weight` to the tally, if - /// the `ballot` contains a valid proof. If the proof is invalid, it will - /// panic. todo: maybe we want to handle these errors? + /// Add a submitted `ballot`, with a specific `weight` to the tally. + /// Remember that a vote is only valid for a specific election (i.e. pair of + /// election public key and crs), and trying to add a ballot validated for a + /// different one will result in a panic. /// /// Note that the encrypted vote needs to have the exact same number of /// options as the initialised tally, otherwise an assert will trigger. #[allow(clippy::ptr_arg)] - pub fn add(&mut self, vote: &EncryptedVote, weight: u64) { - for (ri, ci) in self.r.iter_mut().zip(vote.iter()) { + pub fn add(&mut self, ballot: &Ballot, weight: u64) { + assert_eq!(ballot.vote().len(), self.r.len()); + assert_eq!(ballot.fingerprint(), &self.fingerprint); + for (ri, ci) in self.r.iter_mut().zip(ballot.vote().iter()) { *ri = &*ri + &(ci * weight); } } @@ -140,7 +174,10 @@ impl EncryptedTally { /// Returns a byte array with every ciphertext in the `EncryptedTally` pub fn to_bytes(&self) -> Vec { - let mut bytes: Vec = Vec::with_capacity(Ciphertext::BYTES_LEN * self.r.len()); + let mut bytes: Vec = Vec::with_capacity( + Ciphertext::BYTES_LEN * self.r.len() + ElectionFingerprint::BYTES_LEN, + ); + bytes.extend_from_slice(&self.fingerprint.0); for ri in &self.r { bytes.extend_from_slice(ri.to_bytes().as_ref()); } @@ -150,15 +187,17 @@ impl EncryptedTally { /// Tries to generate an `EncryptedTally` out of an array of bytes. Returns `None` if the /// size of the byte array is not a multiply of `Ciphertext::BYTES_LEN`. pub fn from_bytes(bytes: &[u8]) -> Option { - if bytes.len() % Ciphertext::BYTES_LEN != 0 { + if (bytes.len() - ElectionFingerprint::BYTES_LEN) % Ciphertext::BYTES_LEN != 0 { return None; } - let r = bytes + let fingerprint = + ElectionFingerprint(bytes[0..ElectionFingerprint::BYTES_LEN].try_into().unwrap()); + let r = bytes[ElectionFingerprint::BYTES_LEN..] .chunks(Ciphertext::BYTES_LEN) .map(Ciphertext::from_bytes) .collect::>>()?; - Some(Self { r }) + Some(Self { r, fingerprint }) } } @@ -207,7 +246,10 @@ impl std::ops::Add for EncryptedTally { .zip(rhs.r.iter()) .map(|(left, right)| left + right) .collect(); - Self { r } + Self { + r, + fingerprint: self.fingerprint, + } } } @@ -308,10 +350,21 @@ impl Tally { #[cfg(test)] mod tests { use super::*; - use crate::cryptography::Keypair; + use crate::cryptography::{Keypair, PublicKey}; use crate::encrypted_vote::Vote; + use crate::Ballot; use rand_chacha::ChaCha20Rng; - use rand_core::SeedableRng; + use rand_core::{CryptoRng, RngCore, SeedableRng}; + + fn get_encrypted_ballot( + rng: &mut R, + pk: &ElectionPublicKey, + crs: &Crs, + vote: Vote, + ) -> Ballot { + let (enc, proof) = pk.encrypt_and_prove_vote(rng, &crs, vote); + Ballot::try_from_vote_and_proof(enc, &proof, crs, pk).unwrap() + } #[test] fn encdec1() { @@ -334,13 +387,13 @@ mod tests { println!("encrypting vote"); let vote_options = 2; - let (e1, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); - let (e2, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 1)); - let (e3, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); + let e1 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)); + let e2 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 1)); + let e3 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)); println!("tallying"); - let mut encrypted_tally = EncryptedTally::new(vote_options); + let mut encrypted_tally = EncryptedTally::new(vote_options, ek.clone(), h.clone()); encrypted_tally.add(&e1, 6); encrypted_tally.add(&e2, 5); encrypted_tally.add(&e3, 4); @@ -393,13 +446,13 @@ mod tests { println!("encrypting vote"); let vote_options = 2; - let (e1, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); - let (e2, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 1)); - let (e3, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); + let e1 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)); + let e2 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 1)); + let e3 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)); println!("tallying"); - let mut encrypted_tally = EncryptedTally::new(vote_options); + let mut encrypted_tally = EncryptedTally::new(vote_options, ek, h); encrypted_tally.add(&e1, 1); encrypted_tally.add(&e2, 3); encrypted_tally.add(&e3, 4); @@ -453,12 +506,14 @@ mod tests { println!("encrypting vote"); let vote_options = 2; - let (e1, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); println!("tallying"); - let mut encrypted_tally = EncryptedTally::new(vote_options); - encrypted_tally.add(&e1, 42); + let mut encrypted_tally = EncryptedTally::new(vote_options, ek.clone(), h.clone()); + encrypted_tally.add( + &get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)), + 42, + ); let tds1 = encrypted_tally.partial_decrypt(&mut rng, m1.secret_key()); @@ -502,7 +557,11 @@ mod tests { println!("tallying"); - let encrypted_tally = EncryptedTally::new(vote_options); + let encrypted_tally = EncryptedTally::new( + vote_options, + ElectionPublicKey::from_participants(&[m1.public_key()]), + h, + ); let tds1 = encrypted_tally.partial_decrypt(&mut rng, m1.secret_key()); let max_votes = 2; @@ -551,11 +610,11 @@ mod tests { println!("encrypting vote"); let vote_options = 2; - let (e1, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); - let (e2, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 1)); - let (e3, _) = ek.encrypt_and_prove_vote(&mut rng, &h, Vote::new(vote_options, 0)); + let e1 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)); + let e2 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 1)); + let e3 = get_encrypted_ballot(&mut rng, &ek, &h, Vote::new(vote_options, 0)); - let mut encrypted_tally = EncryptedTally::new(vote_options); + let mut encrypted_tally = EncryptedTally::new(vote_options, ek, h); encrypted_tally.add(&e1, 10); encrypted_tally.add(&e2, 3); encrypted_tally.add(&e3, 40); @@ -584,7 +643,11 @@ mod tests { #[test] fn zero_encrypted_tally_serialization_sanity() { - let tally = EncryptedTally::new(3); + let election_key = ElectionPublicKey(PublicKey { + pk: GroupElement::from_hash(&[1u8]), + }); + let h = Crs::from_hash(&[1u8]); + let tally = EncryptedTally::new(3, election_key, h); let bytes = tally.to_bytes(); let deserialized_tally = EncryptedTally::from_bytes(&bytes).unwrap(); assert_eq!(tally, deserialized_tally);