Skip to content

Commit

Permalink
Simplify voting (#441)
Browse files Browse the repository at this point in the history
* Simplify voting example and default template

* Add CustomInputParameter to init

* Use a tally field to avoid unbounded iteration

* Fix default template readme

* Remove Error:: from ParseError
  • Loading branch information
Victor-N-Suadicani authored Jul 29, 2024
1 parent a605902 commit 5271560
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 281 deletions.
263 changes: 105 additions & 158 deletions examples/voting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,63 @@
//!
//! # Description
//! A contract that allows for conducting an election with several voting
//! options. An `end_time` is set when the election is initialized. Only
//! options. A deadline is set when the election is initialized. Only
//! accounts are eligible to vote. Each account can change its
//! selected voting option as often as it desires until the `end_time` is
//! reached. No voting will be possible after the `end_time`.
//! selected voting option as often as it desires until the deadline is
//! reached. No voting will be possible after the deadline.
//!
//! # Operations
//! The contract allows for
//! - `initializing` the election;
//! - `vote` for one of the voting options;
//! - `getNumberOfVotes` for a requested voting option;
//! - `view` general information about the election.
//!
//! Note: Vec<VotingOption> (among other variables) is an input parameter to the
//! `init` function. Since there is a limit to the parameter size (65535 Bytes),
//! the size of the Vec<VotingOption> is limited.
//! https://developer.concordium.software/en/mainnet/smart-contracts/general/contract-instances.html#limits
#![cfg_attr(not(feature = "std"), no_std)]
use concordium_std::*;

/// The human-readable description of a voting option.
pub type VotingOption = String;
/// The voting options are stored in a vector. The vector index is used to refer
/// to a specific voting option.
pub type VoteIndex = u32;
/// Number of votes.
pub type VoteCount = u32;
//! - Initializing the election (`init` function).
//! - Viewing general information about the election (`view` function).
//! - Voting for one of the voting options (`vote` function).
//! - Tallying votes for a requested voting option (`getNumberOfVotes`
//! function).

/// The parameter type for the contract function `vote`.
/// Takes a `vote_index` that the account wants to vote for.
#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)]
pub struct VoteParameter {
/// Voting option index to vote for.
pub vote_index: VoteIndex,
}
#![cfg_attr(not(feature = "std"), no_std)]

/// The parameter type for the contract function `init`.
/// Takes a description, the voting options, and the `end_time` to start the
/// election.
#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)]
pub struct InitParameter {
/// The description of the election.
pub description: String,
/// A vector of all voting options.
pub options: Vec<VotingOption>,
/// The last timestamp that an account can vote.
/// The election is open from the point in time that this smart contract is
/// initialized until the `end_time`.
pub end_time: Timestamp,
}
use concordium_std::*;

/// The `return_value` type of the contract function `view`.
/// Returns a description, the `end_time`, the voting options as a vector, and
/// the number of voting options of the current election.
/// Configuration for a single election.
#[derive(Serialize, SchemaType, Debug, PartialEq, Eq)]
pub struct VotingView {
pub struct ElectionConfig {
/// The description of the election.
pub description: String,
/// The last timestamp that an account can vote.
/// The election is open from the point in time that this smart contract is
/// initialized until the `end_time`.
pub end_time: Timestamp,
/// A vector of all voting options.
pub options: Vec<VotingOption>,
/// The number of voting options.
pub num_options: u32,
/// All the voting options.
pub options: Vec<String>,
/// The last timestamp at which an account can vote.
/// An election is open from the point in time that the smart contract is
/// initialized until the deadline.
pub deadline: Timestamp,
}

/// The contract state
#[derive(Serial, DeserialWithState)]
#[concordium(state_parameter = "S")]
struct State<S: HasStateApi = StateApi> {
/// The description of the election.
/// `StateBox` allows for lazy loading data. This is helpful
/// in the situations when one wants to do a partial update not touching
/// this field, which can be large.
description: StateBox<String, S>,
/// The map connects a voter to the index of the voted-for voting option.
ballots: StateMap<AccountAddress, VoteIndex, S>,
/// The map connects the index of a voting option to the number of votes
/// it received so far.
tally: StateMap<VoteIndex, VoteCount, S>,
/// The last timestamp that an account can vote.
/// The election is open from the point in time that this smart contract is
/// initialized until the `end_time`.
end_time: Timestamp,
/// A vector of all voting options.
/// `StateBox` allows for lazy loading data. This is helpful
/// in the situations when one wants to do a partial update not touching
/// this field, which can be large.
options: StateBox<Vec<VotingOption>, S>,
/// The number of voting options.
num_options: u32,
/// The smart contract state.
#[derive(Serialize, SchemaType)]
struct State {
/// The configuration of the election.
config: ElectionConfig,
/// A map from voters to options, specifying who has voted for what.
ballots: HashMap<AccountAddress, String>,
/// A map from vote options to a vote count, specifying how many votes each
/// option has.
///
/// Note the data duplication here! The tally can of course always be
/// determined by examining the ballots field. This requires looping
/// over all ballots, counting how many votes exist for a specific option.
/// However, such a loop would be practically unbounded, as it is only
/// limited by the number of votes. Such a loop could exhaust the energy
/// budget of the smart contract functions, potentially making the smart
/// contract unusable and vulnerable to a kind of DDoS attack.
///
/// Thus, we favor duplicating the data in another hashmap, where the vote
/// count can be retrieved and updated in constant time.
tally: HashMap<String, u32>,
}

/// The different errors that the `vote` function can produce.
#[derive(Reject, Serialize, PartialEq, Eq, Debug, SchemaType)]
pub enum VotingError {
/// Raised when parsing the parameter failed.
/// Raised when parsing the input parameter failed.
#[from(ParseError)]
ParsingFailed,
/// Raised when the log is full.
Expand All @@ -108,8 +67,8 @@ pub enum VotingError {
LogMalformed,
/// Raised when the vote is placed after the election has ended.
VotingFinished,
/// Raised when voting for a voting index that does not exist.
InvalidVoteIndex,
/// Raised when voting for an option that does not exist.
InvalidVote,
/// Raised when a smart contract tries to participate in the election. Only
/// accounts are allowed to vote.
ContractVoter,
Expand All @@ -125,114 +84,108 @@ impl From<LogError> for VotingError {
}
}

/// A custom alias type for the `Result` type with the error type fixed to
/// `VotingError`.
pub type VotingResult<T> = Result<T, VotingError>;

/// The event is logged when a new (or replacement) vote is cast by an account.
/// A vote event. The event is logged when a new (or replacement) vote is cast
/// by an account.
#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)]
pub struct VoteEvent {
/// The account that casts the vote.
pub voter: AccountAddress,
/// The index of the voting option that the account is voting for.
pub vote_index: VoteIndex,
}

/// The event logged by this smart contract.
#[derive(Debug, Serialize, SchemaType, PartialEq, Eq)]
pub enum Event {
/// The event is logged when a new (or replacement) vote is cast by an
/// account.
Vote(VoteEvent),
pub voter: AccountAddress,
/// The voting option that the account is voting for.
pub option: String,
}

// Contract functions

/// Initialize the contract instance and start the election.
/// A description, the vector of all voting options, and an `end_time`
/// A description, the vector of all voting options, and a `deadline`
/// have to be provided.
#[init(contract = "voting", parameter = "InitParameter", event = "Event")]
fn init(ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult<State> {
///
/// Note: There is a limit to the size of the function input parameter (65535
/// Bytes), thus the number of voting options and the length of voting options
/// has a limit. Read more here: <https://developer.concordium.software/en/mainnet/smart-contracts/general/contract-instances.html#limits>
#[init(contract = "voting", parameter = "ElectionConfig", event = "VoteEvent")]
fn init(ctx: &InitContext, _state_builder: &mut StateBuilder) -> InitResult<State> {
// Parse the parameter.
let param: InitParameter = ctx.parameter_cursor().get()?;
// Calculate the number of voting options.
let num_options = param.options.len() as u32;
let config: ElectionConfig = ctx.parameter_cursor().get()?;

// Set the state.
Ok(State {
description: state_builder.new_box(param.description),
ballots: state_builder.new_map(),
tally: state_builder.new_map(),
end_time: param.end_time,
options: state_builder.new_box(param.options),
num_options,
config,
ballots: HashMap::default(),
tally: HashMap::default(),
})
}

/// Enables accounts to vote for a specific voting option. Each account can
/// change its selected voting option with this function as often as it desires
/// until the `end_time` is reached.
/// until the `deadline` is reached.
///
/// A valid vote produces an `Event::Vote` event.
/// A valid vote produces an `VoteEvent` event.
/// This is also the case if the account recasts its vote for another, or even
/// the same, option. By tracking the events produced, one can reconstruct the
/// current state of the election.
///
/// It rejects if:
/// - It fails to parse the parameter.
/// - A contract tries to vote.
/// - It is past the `end_time`.
/// - It is past the `deadline`.
#[receive(
contract = "voting",
name = "vote",
mutable,
enable_logger,
parameter = "VoteParameter",
parameter = "String",
error = "VotingError"
)]
fn vote(
ctx: &ReceiveContext,
host: &mut Host<State>,
logger: &mut impl HasLogger,
) -> VotingResult<()> {
) -> Result<(), VotingError> {
// Check that the election hasn't finished yet.
ensure!(ctx.metadata().slot_time() <= host.state().end_time, VotingError::VotingFinished);
ensure!(
ctx.metadata().slot_time() <= host.state().config.deadline,
VotingError::VotingFinished
);

// Ensure that the sender is an account.
let acc = match ctx.sender() {
Address::Account(acc) => acc,
Address::Contract(_) => return Err(VotingError::ContractVoter),
};

// Parse the parameter.
let param: VoteParameter = ctx.parameter_cursor().get()?;

let new_vote_index = param.vote_index;
// Parse the option.
let vote_option: String = ctx.parameter_cursor().get()?;

// Check that vote is in range
ensure!(new_vote_index < host.state().num_options, VotingError::InvalidVoteIndex);
// Check that the vote option is valid (exists).
ensure!(host.state().config.options.contains(&vote_option), VotingError::InvalidVote);

if let Some(old_vote_index) = host.state().ballots.get(&acc) {
let old_vote_index = *old_vote_index;
// Update the tally for the `old_vote_index` by reducing one vote.
*host.state_mut().tally.entry(old_vote_index).or_insert(1) -= 1;
};
let state_mut = host.state_mut();

// Insert or replace the vote for the account.
host.state_mut()
state_mut
.ballots
.entry(acc)
.and_modify(|old_vote_index| *old_vote_index = new_vote_index)
.or_insert(new_vote_index);
.and_modify(|old_vote_option| {
// The account has already voted previously - we should subtract one vote from
// the tally for that option. See doc comment on the tally field for
// why it is done this way.
*state_mut.tally.get_mut(old_vote_option).unwrap() -= 1;

old_vote_option.clone_from(&vote_option);
})
.or_insert(vote_option.clone());

// Update the tally for the `new_vote_index` with one additional vote.
*host.state_mut().tally.entry(new_vote_index).or_insert(0) += 1;
// Now that the account has voted, we should increment the vote count for their
// new vote choice. See doc comment on the tally field for why it is done
// this way.
*state_mut.tally.entry(vote_option.clone()).or_default() += 1;

// Log event for the vote.
logger.log(&Event::Vote(VoteEvent {
voter: acc,
vote_index: new_vote_index,
}))?;
logger.log(&VoteEvent {
voter: acc,
option: vote_option,
})?;

Ok(())
}
Expand All @@ -244,36 +197,30 @@ fn vote(
#[receive(
contract = "voting",
name = "getNumberOfVotes",
parameter = "VoteParameter",
return_value = "VoteCount"
parameter = "String",
return_value = "u32"
)]
fn get_votes(ctx: &ReceiveContext, host: &Host<State>) -> ReceiveResult<VoteCount> {
// Parse the parameter.
let param: VoteIndex = ctx.parameter_cursor().get()?;

// Get the number of votes from the tally.
let result = match host.state().tally.get(&param) {
Some(votes) => *votes,
None => 0,
};
fn get_votes(ctx: &ReceiveContext, host: &Host<State>) -> ReceiveResult<u32> {
// Parse the vote option.
let vote_option: String = ctx.parameter_cursor().get()?;

Ok(result)
// Get the number of votes for this option from the tally map.
// See doc comment on the tally field for why it is done this way.
Ok(host.state().tally.get(&vote_option).copied().unwrap_or_default())
}

/// Get the election information.
#[receive(contract = "voting", name = "view", return_value = "VotingView")]
fn view(_ctx: &ReceiveContext, host: &Host<State>) -> ReceiveResult<VotingView> {
#[receive(contract = "voting", name = "view", return_value = "ElectionConfig")]
fn view(_ctx: &ReceiveContext, host: &Host<State>) -> ReceiveResult<ElectionConfig> {
// Get information from the state.
let description = host.state().description.clone();
let end_time = host.state().end_time;
let num_options = host.state().num_options;
let options = host.state().options.clone();
let description = host.state().config.description.clone();
let options = host.state().config.options.clone();
let deadline = host.state().config.deadline;

// Return the election information.
Ok(VotingView {
Ok(ElectionConfig {
description,
end_time,
deadline,
options,
num_options,
})
}
Loading

0 comments on commit 5271560

Please sign in to comment.