In the previous chapter, you started to enjoy the benefits of using Truffle to improve the development lifecycle:
I decided to introduce Truffle through SimpleCoin to focus exclusively on the functionality of the tool and avoid being distracted by the presentation of new concepts a new contract would have introduced. Also, by having you rewrite in Truffle the same SimpleCoin unit tests you had written in Mocha, I could compare more explicitly the advantages and disadvantages of one framework versus the other.
In this chapter, we’ll go one step further. Now that you’re relatively familiar with Truffle, I believe you’re ready to take advantage of this framework to build an entirely new Dapp from scratch, including a smart contract, some unit tests, and a web UI. We’ll start gently: I’ll give you some background on what functionality the voting contract should provide, and I’ll help you design and implement it. After you’ve completed the contract, I’ll guide you through the usual steps of deploying it within the development environment and unit testing it. Then I’ll show how you can simplify the web UI of an Ethereum Dapp by importing the ABI from a file generated during Truffle compilation—no more copying and pasting ABI and addresses here either!
This chapter is long, but I hope to keep you engaged until the end, especially if you’re eager to learn more. After completing this chapter, you’ll know most of the tool set you need to develop a Dapp from start to end. Now it’s time to start!
A voting Dapp can be simple or complex, depending on the requirements of the elections you wish to support. Voting can be on a small number of preselected proposals (or candidates), or on a potentially large number of proposals suggested dynamically by the voters themselves. The electorate can be made up of a small number of individuals, all known to the organization coordinating the elections, or it can include all the residents of a certain administrative area or of an entire country. In the latter case, a registration process might be necessary to run the election process efficiently and transparently. The outcome might be decided by simple majority: the proposal that gets more votes wins. Or it might require a qualified majority (or quorum): a proposal is passed only if it gets a minimum predefined percentage of votes. The ballot can be secret or open. The vote can be delegated to other individuals or kept exclusively direct. This is only a subset of the options and variants a voting process can include. With technology, the voting process can become even more sophisticated. For example, a vote could be split by the voter into fractions, such that each is assigned to a different proposal, or it could be split similarly, but with each fraction delegated to a different individual.
You might be ambitious and try to design an ultrageneric application that could cater to all possibilities. For our purposes, and to keep the length of this chapter reasonable, I’ll constraint the voting application to a limited set of requirements:
As you might remember from chapter 1, even such a simple voting Dapp has a major advantage over a centralized one: a Dapp decentralizes vote processing and storing and consequently makes tampering much less likely than if voting was running through a centralized application.
Figure 12.1 shows the workflow of the entire voting process. Let’s walk quickly through it:
If you’re eager to develop a more generic voting Dapp, at the end of the chapter I’ll give you a few pointers to help you step into cutting-edge e-voting territory. For now, though, we’ll stick with the constraints I outlined.
Before you start to get your hands dirty, I’ll give you an idea of all the steps you’ll be going through to build your voting Dapp. You can get a more visual idea of the development cycle in figure 12.2, but here’s a list of the steps:
Enough talking—it’s time to get started! Create a new directory within your Ethereum project folder—C:Ethereum ruffleSimpleVoting—then open an OS shell and initialize a Truffle project:
C:Ethereum ruffleSimpleVoting>truffle init
As usual, you should get output like that shown in the screenshot in figure 12.3.
If you’re working on Windows, remember to delete truffle.js; otherwise, delete truffle-config.js. Initially, you’ll be deploying on Ganache, as you did in the previous chapter, so make sure truffle.js (or truffle-config.js) is configured as follows:
module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" // Match any network id } } };
Now you’re ready to create the voting contract!
I believe you could attempt to implement the contract by yourself, based on the requirements outlined earlier. Why don’t you give it a go and then come back here later?
Are you already back? Read on. Arguably, the most important entities involved in an election are the proposal, the voter, and the voting workflow. Let’s look at how to model them.
A registered voter creates a proposal and adds it dynamically to the list of existing proposals during a proposal registration session. The Proposal type should expose a (string) description that its author provides and the (uint) number of votes cast against it. You don’t want to capture the author because they might want to remain anonymous:
struct Proposal { string description; uint voteCount; }
What data would you like to capture about a voter? How about whether the voter has been registered (only white-listed accounts will be allowed to vote), whether they’ve already voted (to prevent double voting), and, if they’ve voted, which proposal they’ve voted for? Try this:
struct Voter { bool isRegistered; bool hasVoted; uint votedProposalId; }
You can easily represent the voting workflow I described earlier in this section with the following enumeration:
enum WorkflowStatus { RegisteringVoters, ProposalsRegistrationStarted, ProposalsRegistrationEnded, VotingSessionStarted, VotingSessionEnded, VotesTallied }
As for most contracts, you’re interested in assigning a management role to the Ethereum address that submits the contract deployment transaction. This will become (at construction) the voting administrator:
address public administrator;
Two main processes take place from the start of the election: the proposal registration session, during which any registered voter is entitled to submit a proposal, and the voting session, which starts immediately afterwards. The voting administrator is responsible for starting and ending each session, presumably on the basis of an agreed start and end time. You’ll capture the status of the voting workflow with this state variable:
WorkflowStatus public workflowStatus;
Obviously, the most important state of the contract has to do with voters and proposals:
mapping(address => Voter) public voters;
Proposal[] public proposals;
The proposal ID’s array index implicitly represents it.
Once votes have been tallied, you want to capture the ID of the winning proposal:
uint private winningProposalId;
This state variable shouldn’t be exposed directly to the Dapp users because its value should be revealed only after votes have been tallied, through getter functions.
You need the state variables you’ve defined to determine the identity of a function caller and the status of the voting workflow. As you know, you can create function modifiers to check the conditions under which a function should be called. You could create a function modifier to check if the caller is the voting administrator
modifier onlyAdministrator() { require(msg.sender == administrator, "the caller of this function must be the administrator"); _; }
or they’re a registered voter:
modifier onlyRegisteredVoter() { require(voters[msg.sender].isRegistered, "the caller of this function must be a registered voter"); _; }
You could create other function modifiers to verify whether voters are being registered
modifier onlyDuringVotersRegistration() { require(workflowStatus == WorkflowStatus.RegisteringVoters, "this function can be called only before proposals registration has started"); _; }
or whether the proposals registration session is active:
modifier onlyDuringProposalsRegistration() { require(workflowStatus == WorkflowStatus.ProposalsRegistrationStarted, "this function can be called only during proposals registration"); _; }
Similarly, you can implement modifiers to verify whether the proposals registration has ended (onlyAfterProposalsRegistration), whether the voting session is active (onlyDuringVotingSession) or has ended (onlyAfterVotingSession), or whether vote tallying has already taken place (onlyAfterVotesTallied). You’ll find all of these modifiers useful for simplifying the logic of the functions you’ll implement shortly.
Because blockchain operations take a few seconds to complete from the moment they’re triggered, it’s useful from a user point of view to get notified as soon as their execution has taken place. Therefore, at the end of each action, the voting contract will publish an event that alters the state of the contract. This will also happen at every workflow status change so that clients, such as a UI, can update their screens accordingly. The contract will publish these events:
event VoterRegisteredEvent (address voterAddress); event ProposalsRegistrationStartedEvent (); event ProposalsRegistrationEndedEvent (); event ProposalRegisteredEvent(uint proposalId); event VotingSessionStartedEvent (); event VotingSessionEndedEvent (); event VotedEvent (address voter, uint proposalId); event VotesTalliedEvent (); event WorkflowStatusChangeEvent ( WorkflowStatus previousStatus, WorkflowStatus newStatus );
At contract construction, you should identify the sender of the contract transaction and make them the voting administrator. You should also set the initial workflow status:
constructor() public { administrator = msg.sender; workflowStatus = WorkflowStatus.RegisteringVoters; }
If you look back at the workflow of the voting process in figure 12.1, you’ll see that the voting administrator is responsible for registering voters:
function registerVoter(address _voterAddress) public onlyAdministrator onlyDuringVotersRegistration { require(!voters[_voterAddress].isRegistered, "the voter is already registered"); voters[_voterAddress].isRegistered = true; voters[_voterAddress].hasVoted = false; voters[_voterAddress].votedProposalId = 0; emit VoterRegisteredEvent(_voterAddress); }
The voting administrator is also responsible for starting and ending the proposal registration session (and, similarly, starting and ending the voting session):
function startProposalsRegistration() public onlyAdministrator onlyDuringVotersRegistration { workflowStatus = WorkflowStatus. ProposalsRegistrationStarted; emit ProposalsRegistrationStartedEvent(); emit WorkflowStatusChangeEvent( WorkflowStatus. RegisteringVoters, workflowStatus); } function endProposalsRegistration() public onlyAdministrator onlyDuringProposalsRegistration { workflowStatus = WorkflowStatus.ProposalsRegistrationEnded; emit ProposalsRegistrationEndedEvent(); emit WorkflowStatusChangeEvent( WorkflowStatus.ProposalsRegistrationStarted, workflowStatus); }
As you can see, function modifiers you defined earlier have simplified the code so much that it has been reduced to only one line and the publishing of a couple of events.
While the proposal registration session is active, registered voters submit proposals with this function:
function registerProposal(string proposalDescription) public onlyRegisteredVoter onlyDuringProposalsRegistration { 1 proposals.push(Proposal({ 2 description: proposalDescription, voteCount: 0 })); emit ProposalRegisteredEvent(proposals.length - 1); }
Also in this case, function modifiers perform the hard work of checking whether the caller is a registered voter and whether the proposal registration session is active. The logic of the function itself is minimal: it creates a new proposal with the provided description and adds it to the proposals array.
Once the voting session has been started, a registered voter can cast their vote with this function:
function vote(uint proposalId) onlyRegisteredVoter onlyDuringVotingSession public { 1 require(!voters[msg.sender].hasVoted, "the caller has already voted"); 2 voters[msg.sender].hasVoted = true; 3 voters[msg.sender].votedProposalId = proposalId; 3 proposals[proposalId].voteCount += 1; 4 emit VotedEvent(msg.sender, proposalId); }
After the administrator has ended the voting session, they can tally the vote with the following function, which finds the ID of the winning proposal and assigns it to the corresponding state variable you defined earlier:
function tallyVotes() onlyAdministrator onlyAfterVotingSession onlyBeforeVotesTallied public { 1 uint winningVoteCount = 0; uint winningProposalIndex = 0; for (uint i = 0; i < proposals.length; i++) { 2 if (proposals[i].voteCount > winningVoteCount) { winningVoteCount = proposals[i].voteCount; winningProposalIndex = i; 3 } } winningProposalId = winningProposalIndex; 4 workflowStatus = WorkflowStatus.VotesTallied; 5 emit VotesTalliedEvent(); emit WorkflowStatusChangeEvent( WorkflowStatus. VotingSessionEnded, workflowStatus); 6 }
It’s convenient to create a set of views—read-only functions that return a slice of the current contract state—for consumption by UI and other clients. For example, during and after the proposal registration session, it would be possible to check the number of proposals and their descriptions with these read-only functions:
function getProposalsNumber() public view returns (uint) { return proposals.length; } function getProposalDescription(uint index) public view returns (string) { return proposals[index].description; }
After votes have been tallied, it would be possible to retrieve the final results (winning proposal ID, description, and vote count) through the following functions:
function getWinningProposalId() onlyAfterVotesTallied public view returns (uint) { return winningProposalId; } function getWinningProposalDescription() onlyAfterVotesTallied public view returns (string) { return proposals[winningProposalId].description; } function getWinningProposalVoteCounts() onlyAfterVotesTallied public view returns (uint) { return proposals[winningProposalId].voteCount; }
Anyone should be entitled to check the winning proposal, not only the administrator or voters, so no modifiers should be present.
It’s also convenient to write other functions to check the identity of the caller. For example, you could write a function that checks whether an address is a registered voter
function isRegisteredVoter(address _voterAddress) public view returns (bool) { return voters[_voterAddress].isRegistered; }
or the administrator:
function isAdministrator(address _address) public view returns (bool){ return _address == administrator; }
And a view of the current voting workflow status could also be useful:
function getWorkflowStatus() public view returns (WorkflowStatus) { return workflowStatus; }
You can see the full contract in listing D.1 in appendix D. I encourage you to enter it in Remix and play with it before placing it under Truffle.
Place the code in listing D.1 (from the book website) in a file named SimpleVoting.sol in the following folder: C:Ethereum ruffleSimpleVotingcontracts. Now, go to the migrations folder: C:Ethereum ruffleSimpleVotingmigrations. Create a new file called 2_deploy_contracts.js and place in it the contents of the following listing, which I’ve adapted from the migration configuration you wrote earlier for SimpleCoin.
var SimpleVoting = artifacts.require("SimpleVoting"); module.exports = function(deployer) { deployer.deploy(SimpleVoting); };
You can now kick the compilation
C:EthereumTruffleSimpleVoting>truffle compile
and you’ll see output similar to this:
Compiling .contractsSimpleVoting.sol... Writing artifacts to .uildcontracts
If you’re experiencing compilation issues, have a look at chapter 11, section 11.2.3, “Troubleshooting truffle compile errors.”
After a successful compilation, start up Ganache in a new OS console:
C:Ethereum ruffleSimpleVoting>ganache-cli
Then go back to the OS console you were using to execute Truffle commands and perform a migration:
C:Ethereum ruffleSimpleVoting>truffle migrate
You’ll see the following output:
Using network 'development'. Running migration: 1_initial_migration.js Deploying Migrations... ... 0xf3ab00eef045a34e2208f6f4dfa218950f7d1a0d30f3f32982febdff55db6a92 Migrations: 0x2f90a477697c9a0144acf862ab7dd98372dd7b33 Saving successful migration to network... ... 0x7af1903172cab2baac3c05f7ff721c8c54aa0f9f34ad64c3a26495bb591c36c9 Saving artifacts... Running migration: 2_deploy_contracts.js Deploying SimpleVoting... ... 0x37ecb76d6cd6051c5d122df53db0063d1336e5b3298150b50d62e8ae845e9bbb SimpleVoting: 0xaf18e4e373b90709cc08a231ce24015a0da4f8cc Saving successful migration to network... ... 0x19ac99d629c4053bf699c34c83c47288666afe6fa8994b90438d0e78ec204c96 Saving artifacts...
By now, you should know how to write unit tests in Truffle. Bear in mind, though, that SimpleVoting’s workflow is more complex than SimpleCoin’s, as described in section 12.1. For example, actions such as registering a proposal or voting can happen only after the administrator has opened the related session, so you should aim some of your tests at verifying that these constraints are being respected. Table 12.1 shows a small sample of what your tests should cover.
Test case |
---|
Only the administrator can register voters. |
You aren’t allowed to register the same voter twice. |
Only the administrator can start the proposal registration session. |
Only the administrator can end the proposal registration session, after starting it. |
Only a registered voter can submit a proposal. |
A registered voter can submit a proposal only after the administrator has started the proposal registration session. |
A registered voter can’t vote before the voting session has started. |
Each of these test cases generally requires that you write at least a negative test that proves an exception is thrown if the underlying conditions aren’t met, and a positive test that verifies the functionality works as expected when all constraints have been met. As an example, if you want to test that “Only the administrator can end the proposal registration session, after starting it,” you should write these tests (with the code shown in listing 12.2):
const SimpleVoting = artifacts.require("./SimpleVoting.sol"); contract('SimpleVoting', function(accounts) { contract('SimpleVoting.endProposalRegistration - onlyAdministrator modifier ', function(accounts) { it("The voting administrator should be able to end the proposal registration session only after it has started", async function() { 1 //arrange let simpleVotingInstance = await SimpleVoting.deployed(); let votingAdministrator = await simpleVotingInstance.administrator(); let nonVotingAdministrator = web3.eth.accounts[1]; try { //act await simpleVotingInstance.endProposalsRegistration( {from: nonVotingAdministrator}); assert.isTrue(false); 2 } catch(e) { //assert assert.isTrue(votingAdministrator != nonVotingAdministrator); assert.equal(e, "Error: VM Exception while processing transaction: revert - the caller of this function must be the administrator"); 3 } }); }); contract('SimpleVoting.endProposalRegistration - onlyDuringProposalsRegistration modifier', function(accounts) { it("An account that is not the voting administrator must not be able to end the proposal registration session", async function() { 4 //arrange let simpleVotingInstance = await SimpleVoting.deployed(); let votingAdministrator = await simpleVotingInstance.administrator(); try { //act await simpleVotingInstance.endProposalsRegistration( {from: votingAdministrator}); assert.isTrue(false); 5 } catch(e) { //assert assert.equal(e, "Error: VM Exception while processing transaction: revert - this function can be called only during proposals registration"); 6 } }); }); contract('SimpleVoting.endProposalRegistration - successful', function(accounts) { it("An account that is not the voting administrator must not be able to end the proposal registration session", async function() { 7 //arrange let simpleVotingInstance = await SimpleVoting.deployed(); let votingAdministrator = await simpleVotingInstance.administrator(); await simpleVotingInstance.startProposalsRegistration( {from: votingAdministrator}); let workflowStatus = await simpleVotingInstance.getWorkflowStatus(); let expectedWorkflowStatus = 1; assert.equal(workflowStatus.valueOf(), expectedWorkflowStatus, "The current workflow status does not correspond to proposal registration session started"); //act await simpleVotingInstance.endProposalsRegistration( {from: votingAdministrator}); let newWorkflowStatus = await simpleVotingInstance .getWorkflowStatus(); let newExpectedWorkflowStatus = 2; //assert assert.equal(newWorkflowStatus.valueOf(), newExpectedWorkflowStatus, "The current workflow status does not correspond to proposal registration session ended"); 8 }); }); });
If you want to run these unit tests, copy the code from listing 12.2 into a file called testSimpleVoting.js in the test folder: C:Ethereum ruffleSimpleVoting est. (I recommend you use the file from the book website.) Then you can execute them as usual
C:Ethereum ruffleSimpleVoting>truffle test
and you’ll see familiar-looking output, as shown in the screenshot in figure 12.4.
The voting website needs two web pages: one for the voting admin, and one for the voters. You’ll create the website by going through these steps:
First of all, open a new OS command shell and create a new directory for the voting web UI, say: C:EthereumSimpleVotingWebUI. In case you didn’t install Bower globally back in chapter 8, install it locally in this directory:
C:EthereumSimpleVotingWebUI>npm install bower
Now import the Web3.js and JQuery libraries into the current directory, as you did in chapter 8. This time, you’ll also install truffle-contract, a library that allows you to import the ABI and contract address seamlessly from the output of truffle compile:
C:EthereumSimpleVotingWebUI>bower install web3#0.20.6 C:EthereumSimpleVotingWebUI>bower install jquery C:EthereumSimpleVotingWebUI>bower install truffle-contract
As you know, Bower will download these libraries into respective directories within the bower_components folder:
bower_components |-- web3 |-- jquery |-- truffle-contract
Now you’ll be able to reference these local copies of Web3.js, jQuery, and truffle-contract from your JavaScript code.
To call the SimpleVoting contract from the web UI, you need to reference its ABI. Truffle generates this during contract compilation in a file called SimpleVoting.json, which is located in this folder: C:Ethereum ruffleSimpleVotinguildcontracts. Create a folder called contracts within your web UI directory: C:EthereumSimpleVoting-WebUIcontracts. Now copy SimpleVoting.json into it.
You might remember that when you created SimpleCoin’s web UI, you copied the ABI manually into the page JavaScript:
var abi = "[{"constant":false,"inputs":[{"name":"_to... var SimpleCoinFactory = web3.eth.contract(JSON.parse(abi)); var simpleCoinInstance = SimpleCoinFactory.at('0x773dc5fc8fc3e...');
Now you’re trying to improve the process of building a web UI, and ideally you’d like to import the contract ABI directly from SimpleVoting.json. But web browsers don’t allow you to read JSON files from your hard drive, so the following JavaScript will fail to execute when loaded directly from your hard drive:
window.onload = function() { $.getJSON("./contracts/SimpleVoting.json", function(json) { SimpleVoting = TruffleContract( json ); ...
This same JavaScript will work as expected, though, if it’s served to the browser through a web server. Bear in mind, Dapp web UIs are served through conventional web servers, so you must set one up to prepare for a realistic deployment.
If you make modifications to your contract (for example, you add a new function), after recompiling it (with truffle compile) and remigrating it (with truffle migrate), you have to copy SimpleVoting.json again from Truffle’s buildcontracts folder to your web project contracts folder. If you shut down Ganache, when you restart it, the deployed instance of Simple-Voting won’t be present, so it’s best to force a new clean migration with truffle migrate --reset and then copy the SimpleVoting.json file across to your web project.
You can easily set up a simple web server, handling only static pages including plain HTML and JavaScript, using Connect and ServerStatic, two Node.js packages, as described in the Stack Overflow article “Using Node.js as a simple web server” (http://mng.bz/oNR2).
First of all, install them into your web UI directory:
C:EthereumSimpleVotingWebUI>npm install connect serve-static
Then, create a file called webserver.js with this code in it:
var connect = require('connect'); var serveStatic = require('serve-static'); connect().use(serveStatic(__dirname)).listen(8080, function(){ console.log('Web server running on 8080...'); });
Finally, start up the website:
C:EthereumSimpleVotingWebUI>node webserver.js
You should see an initialization message:
Server running on 8080...
You’ll now be able to browse any HTML page located in the SimpleVotingWebUI folder through an HTTP connection to localhost on port 8080. For example, create a dummy page called test.html with this markup:
<html> <table><tr><td><b>test</b></td></tr></table> </html>
Then access it through your browser: http://localhost:8080/test.html.
Now that you’ve set up a web server, you’re ready to create the SimpleVoting website. You’ll start from the admin page.
The admin web page should authenticate the user through an Ethereum address. Only the voting administrator address will be allowed to use the page.
When running against Ganache, any password will be valid. But when you move to a public test network, only the valid password associated with the administrator public address will be accepted to execute the contract functionality.
The voting administrator will use this page to
You can get an idea of the layout of this page from figure 12.5.
Although you can look at the entire HTML of this web page on the book website, and I invite you to download and experiment with it (SimpleVotingWebUI folder), I’ll highlight the most frequently recurring elements that you’ll find across both admin and voter pages:
These are the JavaScript files that the admin.html page references:
<head> <script src="bower_components/web3/dist/web3.min.js"></script> <script src="bower_components/jquery/dist/jquery.min.js"></script> <script src="bower_components/truffle-contract/dist/truffle-contract.js"></script> <script src="./simplevoting.js"></script> </head>
As you can see, the admin page references Web3.js, jQuery, and truffle-contract through their respective Bower download folders. For simplicity, and to avoid code duplication, I’ve decided to place all the JavaScript needed by the admin and voter web pages in a single file called simplevoting.js. I’ll present it to you in the next section.
At the top of the page, a row displays the current status of the voting workflow; for example, “Registering Voters” or “Proposal Registration Session Open:”
<table border="0" cellpadding="0" id='currentStatus'> <tr> <td><b>Current status:</b></td> <td id='currentWorkflowStatusMessage'></td> <td></td> <td></td> </tr> </table>
This is useful from two points of view:
The page captures the administrator address and password through input fields:
<table border="0" cellpadding="0" id='user'> <tr> <td><b>Admin address:</b></td> <td><input type="text" id="adminAddress" width="400" /></td> <td><b>password:</b></td> <td><input type="text" id="adminPassword" width="400" /></td> <td><button onclick="unlockAdmin()">Unlock account</button></td> <td id='adminMessage'></td> </tr> </table>
This input is needed to
When connecting to Ganache, it isn’t necessary to unlock the administrator account to perform operations on the web page, but when you connect to the test network, you’ll have to do so.
Input fields also capture input specific to the functionality provided by the page, such as the registration of voters; for example, the field for the voter address:
<td>Voter address:</td> <td><input type="text" id="voterAddress" width="400" /></td>
Some buttons, such as the one associated with the voter registration, trigger a Java-Script function (in this case registerVoter()), which gathers the associated input (in this case the voter address) and includes it in the contract call together with the administrator address:
<td><button onclick="registerVoter()">Register</button></td>
Other buttons, such as the one that starts the voting session, trigger a JavaScript function (startVotingSession()) that doesn’t take any input apart from the administrator address before creating and submitting the related contract call:
<table border="0" cellpadding="0" id='proposalsRegistration'> <tr> <td><button onclick="startProposalsRegistration()">Start</button></td> <td><button onclick="endProposalsRegistration()">End</button></td> <td id='proposalsRegistrationMessage'></td> </tr> </table>
Next to most buttons is a cell to show error or success messages that the JavaScript validating the input or the contract call return:
<td id='voterRegistrationMessage'></td>
As I mentioned previously, you can find the full HTML of the admin.html page on the book website. I encourage you to examine the code before running it.
Before starting this section, I’d like to make it clear I’ll be presenting JavaScript code based on asynchronous calls for two reasons:
I believe that even if you aren’t familiar with asynchronous JavaScript, you should be able to follow this section. But if you’re struggling, I’ve also implemented a synchronous version of the code I’ll be presenting, which you can find on the book website.
I’ll present only the JavaScript around the registration of a voter address by the administrator. This is an interesting use case because it requires common functionality needed by most administrator operations, such as JavaScript code that
Once you’ve understood this use case, you should be able to implement the rest of the administrator functionality by yourself. I encourage you to do so and then compare your code with mine from the book website. Let’s go step by step.
The JavaScript code connecting the web page to the voting contract is similar to the code you wrote for connecting to SimpleCoin back in chapter 8. But, thanks to truffle-contract
"networks": { "1526138147842": { "events": {}, "links": {}, "address": "0xaf18e4e373b90709cc08a231ce24015a0da4f8cc", "transactionHash": "0x37ecb76d6cd6051c5d122df53db0063d1336e5b3298150b50d62e8ae845e9bbb" }, ... "1526683622193": { 1 "events": {}, "links": {}, "address": "0xdaef7d6211bc0c0639178f442c68f468494b7ea2", 2 "transactionHash": "0xea66f69e35ccc77845405dbc183fc0c3ce831cd977f74c5152d6f97a55ebd8af" }
Here’s how the code looks:
var SimpleVoting; 1 window.onload = function() { $.getJSON("./contracts/SimpleVoting.json", function(json) { 2 SimpleVoting = TruffleContract( json ); 3 SimpleVoting.setProvider( new Web3.providers.HttpProvider( "http://localhost:8545")); 4
As I mentioned earlier in section 12.7.1, the jQuery instruction to read SimpleVoting.json will fail if you try to browse on the admin.html page directly from disk, but it will work if you browse through the website you created earlier on:
http//localhost:8080/admin.html
The voting workflow status (“Registering Voters,” “Proposals Registration Started,” and so on) is displayed at page load and refreshed at each status change with this JavaScript function:
function refreshWorkflowStatus() { SimpleVoting.deployed() 1 .then(instance => instance.getWorkflowStatus()) 2 .then(workflowStatus => { var workflowStatusDescription; switch(workflowStatus.toString()) 3 { case '0': workflowStatusDescription = "Registering Voters"; break; case '1': workflowStatusDescription = "Proposals Registration Started"; break; ... default: workflowStatusDescription = "Unknown Status"; } $("#currentWorkflowStatusMessage").html( workflowStatusDescription); 4 }); }
As you know, because the contract function getWorkflowStatus() is a read-only view, its invocation is considered a plain call and doesn’t generate a blockchain transaction. As a result, you don’t need to specify the caller and set other transaction details, such as the gas limit.
You’ll call the workflow status function in two places. The first one is at the end of the code block setting the SimpleVoting reference to the instance of the voting contract, which happens at page load:
$.getJSON("./contracts/SimpleVoting.json", function(json) { ... ... refreshWorkflowStatus(); });
The other one is in the event handler associated with the WorkflowStatusEvent event that the voting contract publishes at every status change. We’ll examine that later.
As you might remember, to invoke any contract function that alters contract state and consequently generates a blockchain transaction, you need to unlock the account of the caller (who becomes the transaction sender). Before performing any state-changing operation on this web page, such as registering a voter or starting the voting session by clicking on the corresponding button, you need to unlock the administrator account. This unlocking takes place through the following function:
function unlockAdmin() { var adminAddress = $("#adminAddress").val(); var adminPassword = $("#adminPassword").val(); var result = web3.personal.unlockAccount( adminAddress, adminPassword, 180); 1 if (result) $("#adminMessage").html('The account has been unlocked'); else $("#adminMessage").html('The account has NOT been unlocked'); }
As you might recall, I’ve registered the execution of this function to the click event of the Unlock Account button:
<td><button onclick="unlockAdmin()">Unlock account</button></td>
As a result, you must click this button before performing any state-changing operation. But if you prefer, you can get rid of the button and call this function just before any contract call that generates a transaction. For example, you could place it just before the contract call instance.registerVoter() within your JavaScript registerVoter() function:
function registerVoter() { ... unlockAdmin(adminAddress, adminPassword); SimpleVoting.deployed() .then(instance => instance.registerVoter(voterToRegister, ...
As for centralized applications, it’s good practice to validate user input before passing it to an external function call, to avoid having bad input generate an exception. To implement that, the first few lines of your registerVoter() JavaScript function are dedicated to capturing user input from the HTML and checking it:
function registerVoter() { $("#voterRegistrationMessage").html(''); var adminAddress = $("#adminAddress").val(); 1 var voterToRegister = $("#voterAddress").val(); 1 SimpleVoting.deployed() .then(instance => instance.isAdministrator( adminAddress)) 2 .then(isAdministrator => { if (isAdministrator) { return SimpleVoting.deployed() .then(instance => instance.isRegisteredVoter( voterToRegister)) 3 .then(isRegisteredVoter => { if (isRegisteredVoter) $("#voterRegistrationMessage") .html( 'The voter is already registered'); 4 else { ... } }); } else { $("#voterRegistrationMessage") .html( 'The given address does not correspond to the administrator'); 4 } }); }
As you can see, you can call some contract read-only view functions to validate the input you’re going to then submit to a transaction-generating function. You might also want to validate if the JavaScript registerVoter() function is being called during the correct workflow step (voter registration) to avoid contract-side exceptions you’ll receive if you attempt to register a voter when the proposal registration session has already started; for example:
return SimpleVoting.deployed() .then(instance => instance.getWorkflowStatus()) .then(workflowStatus => { if (workflowStatus > 0) $("#voterRegistrationMessage") .html('Voters registration has already ended'); else { ...
You’ve performed substantial validation. Now you can call the contract registerVoter() with more confidence that it won’t throw an exception.
You might wonder whether a user might be able to hack the Dapp if they can modify the JavaScript and bypass the validation. This would be pointless. The purpose of JavaScript input validation isn’t to provide a layer of security but to avoid unnecessary transaction costs the user will incur when their transaction is reverted by contract-side validation code. For example, trying to register a voter from a nonadministrator account by bypassing the JavaScript performing the isAdministrator() check will end in an exception being thrown by the contract-side onlyAdministrator function modifier.
While you’re here, I’ll tell you more about JavaScript code around the execution of a contract transaction such as instance.registerVoter(). As you know, when calling a function that alters contract state, you’re generating a blockchain transaction. In this case, you must supply
You can see all these details in the registerVoter() call:
SimpleVoting.deployed() 1 .then(instance => instance.registerVoter( voterToRegister, 2 {from:adminAddress, gas:200000})) 3 .catch(e => $("#voterRegistrationMessage") .html(e)); 4
Before leaving this section on the JavaScript of the admin page, I’d like to show you how to handle workflow status change events that the contract publishes. This would allow you to, for example, update the current workflow status description at the top of the page and show registration confirmations for each voter that the administrator adds.
First of all, you must declare a variable to reference the contract WorkflowStatusChangeEvent event type:
var workflowStatusChangeEvent;
You instantiate this variable at page load, after having set the reference to the contract:
window.onload = function() { $.getJSON("./contracts/SimpleVoting.json", function(json) { ... SimpleVoting.deployed() .then(instance => instance .WorkflowStatusChangeEvent()) 1 .then(workflowStatusChangeEventSubscription => { workflowStatusChangeEvent = workflowStatusChangeEventSubscription; workflowStatusChangeEvent .watch(function(error, result){ 2 if (!error) refreshWorkflowStatus(); 3 else console.log(error); }); }); ...
As you can see, every time the workflow status changes, the function refreshing its description is invoked. The contract event that notifies a voter registration confirmation, VoterRegisteredEvent(), is handled in exactly the same way, with the only difference being that the UI is refreshed inline and not through a client-side function:
SimpleVoting.deployed() .then(instance => instance.VoterRegisteredEvent()) .then(voterRegisteredEventSubscription => { voterRegisteredEvent = voterRegisteredEventSubscription; voterRegisteredEvent.watch(function(error, result) { if (!error) $("#voterRegistrationMessage") .html('Voter successfully registered'); 1 else console.log(error); }); });
The voter page, which you can see in figure 12.6, shares many elements in common with the admin page. As I said earlier, the main purpose of the voter page is to register proposals and to vote on them. It’s worthwhile to highlight the proposal table, which is updated dynamically every time a new proposal is added by a voter:
<tr><table border="0" cellpadding="0" width="600" id='proposalsTable'> </table></tr>
Let’s see how the proposal table update takes place. If you have a look at the register-Proposal() function in simplevoting.js, you’ll notice many similarities to registerVoter() that we examined earlier. The main difference is that the event handler associated with the event that the contract fires upon proposal registration, ProposalRegisteredEvent(), doesn’t only confirm that the registration has taken place by refreshing the related HTML label. In addition, it calls a function that generates a dynamic HTML table listing of all the proposals added so far:
... proposalRegisteredEvent.watch(function(error, result) { if (!error) { $("#proposalRegistrationMessage") .html('The proposal has been registered successfully'); 1 loadProposalsTable(); 2 } else console.log(error); });
Here’s how the proposals table is refreshed dynamically:
function loadProposalsTable() { SimpleVoting.deployed() .then(instance => instance.getProposalsNumber()) .then(proposalsNumber => { var innerHtml = "<tr><td><b>Proposal ID</b></td><td><b>Description</b></td></tr>"; j = 0; for (var i = 0; i < proposalsNumber; i++) { getProposalDescription(i) .then(description => { innerHtml = innerHtml + "<tr><td>" + (j++) + "</td><td>" + description + "</td></tr>"; $("#proposalsTable").html(innerHtml); }); } }); } function getProposalDescription(proposalId) { return SimpleVoting.deployed() .then(instance => instance.getProposalDescription(proposalId)); }
Now that you understand the code for both web pages, it’s time to run them. I’ll guide you through the entire voting workflow so you’ll see how the application works from start to end.
Before starting, make sure Ganache is running in a console. I suggest you start it with an instruction to redirect the output to a log file, which will come in handy later:
C:Ethereum ruffleSimpleVoting>ganache-cli > c: empganache.log
Because you’ve restarted Ganache, you must redeploy SimpleVoting (in a separate console):
C:Ethereum ruffleSimpleVoting>truffle migrate --reset
Next, recopy SimpleVoting.json from Truffle’s buildcontracts folder to the website’s contracts folder. Now you can restart the website (in a separate console):
C:EthereumSimpleVotingWebUI>node webserver.js Server running on 8080...
Browse the admin webpage
http://localhost:8080/admin.html
and you’ll see the screen I showed you earlier in figure 12.5. You might have noticed that the initial current status is “Registering Voters,” as expected, as you can see in figure 12.7.
Unless your truffle.js is configured to run the migrations from a specific account, they’ll be executed against accounts[0], which, as you know, will become the voting administrator. Because you’re deploying on Ganache, look at the ganache.log file you’re redirecting the output to:
Ganache CLI v6.1.0 (ganache-core: 2.1.0) Available Accounts ================== (0) 0xda53708da879dced568439272eb9a7fab05bd14a (1) 0xf0d14c6f6a185aaaa74010d510b975ca4caa1cad (2) 0x5d6449a4313a5d2dbf7ec326cb8ad204c97413ae ...
Copy the address corresponding to account (0) into the Admin Address text box and, because you’re running against Ganache, leave the password field empty, as shown in figure 12.8. You can then register the voters’ addresses.
Because shortly you’ll be running some voter functionality, I suggest you register (1), (2), (3), ... from Ganache’s startup screen as voters. When registering account (1), you enter the corresponding address in the Voter Address text box, as shown in figure 12.9.
Before submitting the voter registration transaction, you must unlock the admin account, so click Unlock Account. You’ll get a confirmation that the account has been unlocked. Then click Register. If everything has gone well, you should get a confirmation message next to the Register button; otherwise, you’ll get an error message.
Register a few voters using the other account addresses from Ganache’s log file. If you do so within the next three minutes, you don’t need to unlock the administrator account again before registering the accounts.
As I said earlier, when running on Ganache, you don’t need to enter any administrator password, and you don’t even need to unlock the corresponding account, as accounts are all indefinitely unlocked. But it’s best to get used to the account unlock operation for a smoother transition to the public test network later.
If you want to verify that these accounts are indeed registered, you can use the Check Registration verification area. For example, to verify that account (1) is a registered voter, enter the corresponding address in the Address text box and click Check Registration. You should see a confirmation message like the one shown in figure 12.10.
Before we move to the next workflow step, I suggest you see what happens if you try to
You should receive corresponding error messages.
Once you’ve registered a few voters, you can start the proposal registration session by clicking Start in the related area of the screen. This will call the JavaScript start-Proposals-Registration() function, which in turn will call the start-Proposals-Registration() contract function. The contract function will raise the WorkflowStatusChangeEvent event, which the client-side JavaScript refreshWorkflow-Status() function that’s in charge of refreshing the workflow status label to “Proposals Registration Started” will handle, as you can see in figure 12.11.
At this point, voters are allowed to register proposals. Open the voter web page in a separate browser tab: http://localhost:8080/voter.html.
You’ll see a screen similar to the one shown in figure 12.6, with a small difference: the current status will be “Proposals Registration Started” as on the Admin page. This confirms the page is connected correctly to the voting contract.
Now register a proposal under Ganache’s account (1), who is a valid registered user. Enter the corresponding address in the Voter Address text box, and leave the password blank, as you did for the administrator. Then enter a proposal description, for example, “Proposal Zero.” If you want, before clicking Register, you can check what happens if you submit a vote at this stage (say against proposal ID = 0). You should get an error message.
Now click Register. You’ll get a confirmation message next to the button. You’ll also see at the top of the screen the proposal you’ve entered together with the related ID, returned from the contract, as shown in figure 12.12.
Keep registering more proposals, using all the accounts you’ve registered as voters. If you want, you can register more than one proposal against the same account, as you didn’t set any constraints on this. You can also check what happens if you try to register a proposal against the administrator account or against some account that you didn’t register as a user.
After registering a few proposals, you can end the proposal registration session. Go back to the admin page and, if the administrator account address is still in the related text box, click End in the Proposal Registration Session area. You should get a confirmation message next to the button and the new workflow status in the Current Status area, as shown in figure 12.13.
As usual, you can try to attempt actions that aren’t supposed to take place at this stage. For example, try ending the voting session.
Time to start the voting session! Click Start corresponding to the voting session, and the Current Status description at the top of the screen will change accordingly to “Voting Session Started.”
Go back to the voter’s screen. You’ll see that the status at the top of the screen has also changed to “Voting Session Started.” This confirms that it has been refreshed by the WorkflowStatusChangeEvent event that the contract published at the end of the startVotingSession() function and the voter’s page JavaScript handled. At this point, you’re allowed to vote.
Enter the address of one of the voter accounts in the Voter Address text box, and then put one of the Proposal IDs you see at the top of the screen in the Proposal ID text box. Then click Vote. You’ll see a confirmation message next to the Vote button. If you try to vote again, you’ll see an exception from the voting contract that’s preventing you from voting twice. Continue by casting various votes from the accounts you registered earlier.
After you’ve voted from various registered accounts, you can end the voting session the same way you started it: go back to the admin page, move the mouse in the Voting Registration area, and click End. As usual, you’ll see a confirmation message and a change of state at the top of the screen. If you now try to go back to the voter page and cast a vote through a registered account that you didn’t use, you’ll be prevented from voting.
It’s the moment of truth: time to tally the votes! On the admin page, click Tally Votes. Apart from the usual confirmation message and status description stage, you’ll see a new table appearing at the bottom of the screen summarizing the vote results. The handler of the WorkflowStatusChangeEvent event has generated this table, with a specific check on the status ID corresponding to votes tallied.
If you flip back to the voter page, you’ll see the same results table at the bottom of the screen, as you can verify in figure 12.14. The same table appears because both pages were listening to the same event.
Congratulations! You’ve designed, implemented, tested, and run a full voting Dapp. You should consider this an important achievement. It’s the first time (in the book) you’ve built a complete Dapp from scratch, covering all the layers from the smart contract to the web UI. You’ve come a long way from chapter 1, when you started to learn about new concepts, such as blockchain and Dapp, and experimented with a simple embryonic version of SimpleCoin. Now you should have a good understanding of smart contracts, how to write them in Solidity, how to communicate with them with Web3.js, and how to create a simple web UI to interact with them in an easier way. Before leaving the chapter, you’ll deploy the Dapp into the public test network and check that everything is still working as expected.
Here are the steps you must take to deploy SimpleVoting onto Ropsten, the public test network:
You should be able to do everything pretty much by yourself but, just in case, let me walk through these steps with you. Start with an easy one: press CTRL+C or close down the Ganache window.
You’ve done this a couple of times already, but I’ll save you some page flipping. You can see in the following code the full command to connect to Ropsten through some seed nodes, including the option to perform a fast synchronization, in case you haven’t been connected to the test network for a while. Also, I’ve highlighted the last two parameters, which open geth’s communication through RPC on port 8545 and allow cross-origin resource sharing (CORS) so that the JavaScript of your web page will be allowed to communicate directly with geth:
C:Program Filesgeth>geth --testnet –-bootnodes "enode://145a93c5b1151911f1a232e04dd1a76708dd12694f952b8a180ced40e8c4d25a908 a292bed3521b98bdd843147116a52ddb645d34fa51ae7668c39b4d1070845@188.166.147 .175:30303,enode://2609b7ee28b51f2f493374fee6a2ab12deaf886c5daec948f122bc837 16aca27840253d191b9e63a6e7ec77643e69ae0d74182d6bb64fd30421d45aba82c13bd@13 .84.180.240:30303,enode://94c15d1b9e2fe7ce56e458b9a3b672ef11894ddedd0c6f247e 0f1d3487f52b66208fb4aeb8179fce6e3a749ea93ed147c37976d67af557508d199d9594c35f [email protected]:30303" --verbosity=4 --syncmode "fast" --cache=1024 --rpc --rpcport 8545 --rpccorsdomain "http://localhost:8080" --rpcapi "eth,net,web3,personal"
If your geth client doesn’t seem to synchronize with the public test network, attach a console to geth and manually add the Ropsten peers specified on this GitHub page: http://mng.bz/6jAZ.
In the previous chapter, I explained how to point Truffle to a test or live network. Modify your truffle.js (or truffle-config.js) file as follows:
module.exports = { networks: { development: { host: "localhost", port: 8545, network_id: "*" // Match any network id }, ropsten: { host: "localhost", port: 8545, 1 network_id: 3, from: "0x70e36be8ab8f6cf66c0c953cf9c63ab63f3fef02", 2 gas: 4000000 3 } } };
Make sure the Ropsten account you choose to deploy SimpleVoting has some (test) Ether in it!
To unlock the Ropsten account you’ve specified for deployment, start by opening a new console and attaching it to geth:
C:Program Filesgeth>geth attach ipc:\.pipegeth.ipc
Once the geth JavaScript console has started, unlock the deployer address (for 300 seconds) as follows:
> web3.personal.unlockAccount("0x70e36be8ab8f6cf66c0c953cf9c63ab63f3fef02", "YOUR_PASSWORD", 300); true
To perform a migration on a network other than the development one, you must provide it explicitly through the --network option (on a separate console):
C:Ethereum ruffleSimpleVoting>truffle migrate --network ropsten
Make sure your geth client is fully synchronized with Ropsten before attempting this operation. You can check the latest Ropsten block against https://ropsten.etherscan.io/ and compare it with what you see on your client’s console.
If your geth client is actively connected to Ropsten and you’ve configured everything correctly, you should see some output confirming that deployment has taken place:
Running migration: 1_initial_migration.js Deploying Migrations... ... 0x8ec9c0b3e1d996dcd2f6f8b0ca07f8ce5e5d15bd0cc2eea3142f351f53070499 Migrations: 0xeaf67f4ce8a26aa432c106c2d59b7dbeaa3108d8 Saving successful migration to network... ... 0xadffddd1afe362a802fc6a80d9c60dc2e4a85f9e850ff3c252307ec9255988af Saving artifacts... Running migration: 2_deploy_contracts.js Deploying SimpleVoting... ... 0xbe1ca60296cc8fb94a6e97c74bf6c20547b2ed7705e8db84d51e17f38904fa09 SimpleVoting: 0x3a437f22d092b6ae37d543dcb765de4366c00ecc Saving successful migration to network... ... 0x37f90733539ba00f946ef18b456b5f7d6b362f93e639583d789acd36c033e5d2 Saving artifacts...
If you look back at the first lines of simplevoting.js, the initialization looks at a web3 provider coming from localhost:8545. Geth is exposing to RPC the same port number that Ganache was previously exposing, so everything should still work, right? Let’s find out! First of all, copy the new SimpleVoting.json from Truffle’s buildcontracts to the website contracts folder. Then try to browse on admin.html as you did previously. (Make sure the website is still running; otherwise, restart it with: node webserver.js.) Type the following:
http://localhost:8080/admin.html
Yes! The page loads correctly, and it shows the initial “Registering Voters” status as expected. You can now go through the whole workflow, from voters’ registration to vote tallying, as you did when running on Ganache. Before doing so, make sure the TESTNET admin and voter accounts have enough Ether to perform the related operations.
Bear in mind that when you perform operations on TESTNET, you must unlock the related account at each operation. Also, every time you perform an operation, from either the admin account or a voter account, you’ll have to wait for the related transaction to be mined, so it will take longer (around 15 seconds) to receive confirmations.
If your UI seems unresponsive and you don’t get a confirmation that the operation you attempted completed, check potential errors on the console of your browser’s development tools. For example, on Chrome, you can click F12 and then select Console from the top menu. If the issue is related to account unlocking, it might be due to changes in Web3 around this area. If that’s the case, you can unlock the administrator or the voter account from an attached Geth console, as you’ve seen in previous chapters, rather than using the Unlock Account button.
I hope you’ve found this chapter useful for tying together all you learned in the previous chapters. At this point, you should feel confident you can start building your own Dapp, and you should be geared up to take on a new challenge. If you’re eager to practice your newly acquired skills a little more before moving on to the next chapter on advanced topics in contract design and security, I’ve prepared for you a little plan you can use as a springboard for improving SimpleVoting. I have two sets of improvements in mind:
I designed SimpleVoting with the main objective of presenting the full Ethereum Dapp development lifecycle in one shot, so I decided to keep it as simple as possible to focus mainly on the big picture. Also, I understand that not all readers have the same background and experience in web development and continuous integration, so I avoided introducing relatively advanced techniques in either of these areas. If you’d like to improve the web UI, there’s room for improvement in various respects, including these two:
Your application shouldn’t be served through a plain web browser, but through a standard Web3 provider, such as Mist or MetaMask, that provides enhanced security. These providers require you to call contract functions asynchronously, though, so reimplementing your SimpleVoting.js using asynchronous features would also be beneficial for this reason. After you’ve reimplemented your JavaScript code accordingly, you can detect a standard provider by replacing this line
SimpleVoting.setProvider( new Web3.providers.HttpProvider( "http://localhost:8545"));
with this:
var web3Provider; if(typeof web3 != 'undefined') web3Provider = new Web3(web3.currentProvider); 1 else web3Provider = new Web3( new Web3.providers.HttpProvider( "http://localhost:8545")); 2 SimpleVoting.setProvider(web3Provider);
How do you supply a standard provider? First, remember to log in. Then, assuming you installed the MetaMask browser plugin, as explained in chapter 3, make my suggested code change at the top of SimpleVoting.js, and finally browse on the admin or voter page. By doing this, the Web3 provider will be set to MetaMask.
If you have experience in using web build tools, you might have found it a bit annoying to copy SimpleVoting.json from Truffle’s buildcontracts to the web project contracts folder every time you made a change. You might be wondering whether Truffle supports a build pipeline. It does, and, in fact, you can plug in your favorite build tool, such as Webpack or Grunt.
The Truffle documentation website also includes a Webpack Truffle Box (https://truffleframework.com/boxes/webpack), which is a template project developed with Webpack support. You can use it as a base to build your Webpack integration, or at least to understand how it works.
The current voting application is intentionally limited from a functional point of view. After the admin registers the voters (one by one), voters can register (potentially many) proposals and then vote on one of them. The winning proposal is decided based on simple majority: the one that gets the most votes is chosen. Also, you’re recording the proposal that each voter chooses, so there’s no anonymity. Here are some ways you could enrich the voting Dapp functionality:
The admin could register voters in a single bulk registration, rather than one by one, saving on administrative time and transaction costs. You could implement this enhancement with a new function taking in an array of addresses.
A voter could delegate their vote to another registered voter. Then the delegator wouldn’t be able to vote, and the person delegated would cast as many votes as they’ve been delegated to cast by as many voters.
These are some hints on how to start the implementation:
Voter { HasVoted: boolean, CanVote: Boolean, NumVotes: int }
delegateVote(address delegatedVoter)
You could introduce the concept of a qualified majority. To win, a proposal should reach a predefined quorum: a minimum percentage of the cast votes.
uint quorum
The current voting Dapp allows a voter account to increment the vote count of one of the proposals. This creates strong coupling between the voter and the chosen proposal, which can be slightly softened through vote delegation. In addition, the delegated voter will assign the entire vote of the delegator to a single proposal.
An interesting concept that has appeared in the crypto space is that of token-based voting. It works like this:
Because a vote has to be “spent,” this would prevent double voting. Things could get more sophisticated. A voter could, for example, do one of the following:
You could build a token-based voting Dapp by implementing a voting token (along the lines of SimpleCoin) and then integrating it into the voting contract in the same way you integrated the crowdsale token in the crowdsale contract.
As you might remember, you’re recording the proposal ID that each voter chooses in the votedProposalId property of the voter struct. If you think you could implement anonymous voting by not recording the proposal chosen by the voter and by incrementing its vote count, you’re wrong. As you know, votes are cast through transactions, which are all recorded in the blockchain and can be inspected, so it would be easy to find out who voted for which proposal.
Anonymizing voting is possible, but it’s complicated because there’s no out-of-the-box functionality for anonymizing transactions. This is a complex topic based on advanced cryptographic concepts such as zero-knowledge proof, which you might have heard associated with popular cryptocurrencies such as Zcoin, Zcash, and Monero.
Some researchers at Newcastle University, UK, have implemented an Ethereum anonymous voting Dapp based on a protocol called Open Vote Network. They’ve made both their paper and their code publicly available on GitHub,[1] together with a video tutorial and many academic references on the topic. I recommend you review this material!
See “Open Vote Network” on GitHub at http://mng.bz/nQze for more information.