Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify voting example and default template #440

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Victor-N-Suadicani marked this conversation as resolved.
Show resolved Hide resolved
//! - `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>,
Victor-N-Suadicani marked this conversation as resolved.
Show resolved Hide resolved
/// 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>,
Victor-N-Suadicani marked this conversation as resolved.
Show resolved Hide resolved
/// 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) {
Victor-N-Suadicani marked this conversation as resolved.
Show resolved Hide resolved
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
Loading