Skip to content

Building a Secret Ballot DApp

Introduction

In this tutorial we'll demonstrate how to write, deploy, and interact with confidential contracts using Truffle and Web3c.js

To demonstrate the basics of confidential smart contracts we'll walk through development of a secret ballot DApp. This DApp allows a user, called the ballot creator, to create a new ballot with a list of candidates. Users can vote for candidates by sending transactions to the smart contract. At the end of the voting period (determined by the ballot creator) the vote totals are revealed, but individual votes remain private.

This DApp benefits from confidentiality in several ways. As described in the confidential smart contract overview, the state of a confidential contract is secret and cannot be viewed by anyone unless the smart contract reveals it. Thus, our smart contract can define when vote totals should be revealed publicly, and nobody (not even the ballot creator) can access the information before that time.

As with standard smart contracts, the code of a confidential smart contract is public and can be audited by anyone. In our secret ballot DApp, this means anyone can verify that the voting is fair (i.e. nobody can manipulate votes, and the reported winner is correct) even though individual votes are never revealed to anyone.

If you don't want to go through each step, feel free to download the finished code:

git clone https://github.com/oasislabs/secret-ballot

To jump to the end, visit the Secret Ballot to play with a live demo.

Prerequisites

Oasis Contract Kit

This tutorial requires the Oasis Contract Kit, which has necessary tools and dependencies pre-installed and ready to go. Please follow these instructions to install Contract Kit. If you prefer to modify an existing project, refer to the Truffle tutorial.

Tip

The commands in this tutorial should be run inside the Contract Kit environment.

Truffle

This tutorial uses the Truffle framework to develop and test smart contracts. If you have never used Truffle before we recommend you review this Quickstart guide. Truffle is included as part of Contract Kit.

Solidity

This example is implemented in Solidity. An overview of the Solidity language is beyond the scope of this tutorial. A reference is available here. The Solidity compiler is included as part of Contract Kit.

Funded MetaMask Wallet

Install MetaMask here and retrieve funds from the Oasis faucet.

Initialize Your Project

To start, let's initialize our secret-ballot project directory.

# Create a new directory for our project
mkdir secret-ballot && cd secret-ballot

# Configure an empty Truffle project in the current directory
truffle unbox oasislabs/oasis-box

Note

The Truffle box oasislabs/oasis-box is a special project template allowing Truffle to support the full spectrum of Oasis Devnet features, including both Solidity and Rust compilation as well as confidential smart contracts. It's suggested to start new Truffle projects using this command (rather than truffle init) to ensure compatibility with the Oasis Devnet.

Our project has the following structure:

Directory Description
/contracts Contract source code. Solidity contracts have a .sol extension. Rust contracts are named lib.rs and have a separate directory per contract (described below). You can mix Solidity and Rust in a single project.
/migrations Scripts for deploying contracts. Executed when you run truffle migrate
/tests Code for testing contracts. Executed when you run truffle test

You'll notice these directories are empty because we haven't created any contracts or tests yet. We'll do this in the next step.

Creating a Secret Ballot Contract

Secret ballots are created by a user, called the ballot creator, who supplies a list of candidates and controls when voting ends:

// contracts/confidential_SecretBallot.sol

pragma solidity ^0.4.18;

contract SecretBallot {
  // The address of the account that created this ballot.
  address public ballotCreator;

  // Is voting finished? The ballot creator determines when to set this flag.
  bool public votingEnded;

  // Candidate names
  bytes32[] public candidateNames;

  // ... (code omitted for space)

  constructor(bytes32[] _candidateNames) public {
    ballotCreator = msg.sender;
    votingEnded = false;
    candidateNames = _candidateNames;
  }

  // ... (code omitted for space)
}

Any account can privately vote for a given candidate once and only once while the voting is open:

  // Keep track of which addresses have voted already to prevent multiple votes.
  mapping (address => bool) public hasVoted;

  // Tallies for each candidate
  mapping (bytes32 => uint16) private votesReceived;

  // The total number of votes cast so far. Revealed before voting has ended.
  uint16 public totalVotes;

  // ... (code omitted for space)

  function voteForCandidate(bytes32 candidate) public {
    require(!votingEnded);
    require(validCandidate(candidate));
    require(!hasVoted[msg.sender]);

    hasVoted[msg.sender] = true;
    votesReceived[candidate] += 1;
    totalVotes += 1;
  }

Warning

Although the DApp prevents duplicate votes from a single address, it does not prevent users from casting multiple votes by creating new accounts.

Only after the ballotCreator ends the voting period, any account can retrieve the results of the ballot:

  function endVoting() public returns (bool) {
    require(msg.sender == ballotCreator);  // Only the ballot creator can end the vote.
    votingEnded = true;
    return true;
  }

  function totalVotesFor(bytes32 candidate) view public returns (uint16) {
    require(validCandidate(candidate));
    require(votingEnded);  // Don't reveal votes until voting has ended
    return votesReceived[candidate];
  }

To see the full source code, see contracts/confidential_SecretBallot.sol here.

Compiling a Confidential Contract

Our compiler will look for a specific filename prefix to determine if the contract should be compiled and deployed as a confidential smart contract. For Solidity contracts, Truffle looks at the name of the .sol file; for Rust contracts, it uses the directory name.

Solidity

Filename Confidential Smart Contract
MyContract.sol
confidential-MyContract.sol

Rust

Directory name Confidential Smart Contract
my-contract/*
confidential-my-contract/*

Note: You can also use underscore instead of hyphen, i.e. confidential_.

To develop a confidential smart contract, name your file according to convention above. You can then use standard Truffle commands for compiling and deploying.

To compile our confidential contract, just run the following command:

truffle compile

You've just compiled your first confidential contract! :)

Deploying a Confidential Contract Using Truffle

Now that we can compile confidential contracts, lets deploy it to the Oasis Devnet.

Note

Deployments and testing for confidential smart contracts currently need to be run against the Oasis Devnet. We are working on adding confidentiality support to our local chain for testing purposes.

If you have not done so yet, follow the tutorial here to set up and fund your MetaMask wallet. You should copy the mnemonic used by MetaMask into truffle-config.js, so that Truffle can use the same wallet that you have funded from the faucet.

Next, we need to write a migration script to deploy the contract. We use migration scripts here for two purposes. First, to specify the constructor arguments when deploying our contract. Second, to keep track of where our contracts have been deployed to, and a path for handling upgrading to new versions of those contracts.

To create your migration, create a file named migrations/2_secret_ballot_migration.js with the following contents:

const SecretBallot = artifacts.require("SecretBallot");

module.exports = function(deployer) {
  const candidates = [
    web3.utils.fromAscii("Bulbasaur"),
    web3.utils.fromAscii("Charmander"),
    web3.utils.fromAscii("Squirtle")
  ];
  deployer.deploy(SecretBallot, candidates);
}

This is no different from a normal Truffle deploy process, where we pass in the contract artifact along with the contract's constructor arguments into deployer.deploy. Here, we are configuring the deployment of our SecretBallot contract with a list of three candidates, Bulbasaur, Charmander, and Squirtle.

Finally, run the migration to deploy the contract to the Oasis Devnet.

truffle migrate --network oasis

You should see output that looks like

2_secret_ballot_migration.js
============================

   Deploying 'SecretBallot'
   ------------------------
   > transaction hash:    0xcdea74a20a24f00530ec043c770fc080eee55867fe557ff6e9aa6675e4e0a650
   > Blocks: 0            Seconds: 0
   > contract address:    0x429a875530142ba06656963457e7708B43d43E63
   > account:             0x064F1ca13d30267CF8D377bcFe80eF53CadAb2FB
   > balance:             6.233363295
   > gas used:            187925
   > gas price:           1 gwei
   > value sent:          0 ETH
   > total cost:          0.000187925 ETH


   > Saving migration to chain.
   > Saving artifacts
   -------------------------------------
   > Total cost:         0.000187925 ETH

This output means that your contract has successfully been deployed. Now, take note of your contract address. We'll use it later in this tutorial.

For more info on Truffle migrations, see the Truffle docs.

Testing your Confidential Contract

Having written, compiled, and deployed our confidential contract, lets go ahead and test it so that we can verify its correctness.

Now, create a test file test/test_secret_ballot.js with our first test

// test_secret_ballot.js
const SecretBallot = artifacts.require("SecretBallot");
const Web3c = require("web3c");
const truffleConfig = require("../truffle-config.js");

contract("SecretBallot", (accounts) => {
  // Creates an instance of web3c to use in the tests with a provider that
  // makes and signs requests. The provider is configured with a private key
  // (derived from a mnemonic) and gateway url.
  const web3c = new Web3c(truffleConfig.networks.oasis.provider());

  // Creates an instance of our secret ballot contract, communicating with the contract
  // at the deployed address. Note: this address will be different in tests since it
  // will be redeployed via migration before every run of truffle test.
  const ballotInstance = new web3c.oasis.Contract(SecretBallot.abi, SecretBallot.address, {
    // the default "from" address of all transactions originating from this contract
    from: accounts[0]
  });

  // Notice how the totalVotes variable is *public* in the contract code
  // and so anyone can make encrypted calls to it *before* the voting period
  // has ended.
  it('Should allow voting for a candidate', async () => {
    // performs an encrypted call to read public data
    let totalVotes = await ballotInstance.methods.totalVotes().call();
    assert.equal(totalVotes, 0);

    // sends an encrypted transactions, updating the private data
    await ballotInstance.methods.voteForCandidate(web3c.utils.fromAscii("Charmander")).send();

    // reading the public data again should now be updated
    totalVotes = await ballotInstance.methods.totalVotes().call();
    assert.equal(totalVotes, 1);
  });
});

To run your tests against the Oasis Devnet, issue the command

truffle test --network oasis

Note

Deployments and testing for confidential smart contracts currently need to be run against the Oasis Devnet. We are working on adding confidentiality support to our local chain in future releases. You can also deploy the contract without confidentiality to a local chain for Truffle testing, since the logic should be quite similar in most cases. See the Contract Kit tutorial on how to use the local chain without confidentiality. Note that this may break your end-to-end DApp if you are using web3c.js.

Application

So far we've covered the basics of developing confidential contracts on Oasis.

  • We've developed a SecretBallot contract with private data in Solidity
  • We've compiled, deployed, and tested the contract with Truffle
  • We've communicated with the confidential smart contract through an encrypted channel using web3c.js.

Now, let's see it in action with a working DApp. Retrieve your deployed contract address from the previous truffle migrate --network oasis step. Then visit the following URL, entering your deployed contract address.

https://cdn.oasiscloud.io/ballot/index.html?ballot=[CONTRACT_ADDRESS]

There, you should be able to see all of the candidates added in your contract migration. You should also be able to vote for one of them. Once you end the voting period, the confidential results will be revealed.

Reminder: in our contract logic we've restricted the ability to end the voting period to the account that deployed the contract. Make sure your active Metamask account is the same as the one Truffle is using (i.e. the mnenmonic used in truffle-config.js).

Since the DApp frontend is just a static web app, you can also deploy it yourself. The source code can be found here.

Extending the Ballot

This example can be modified to support many other types of elections, for example:

  • To reveal only the winner (or ranking) without revealing the vote tallies
  • To reveal each voter's actual vote only to a designated user (e.g. the ballot creator or an independent auditor) while keeping it private from the rest of the world
  • To allow only a specific set of users to cast votes

These features would be very difficult or impossible to achieve on blockchain platforms such as Ethereum in which all state is public.

Bonus: Web3c.js Deploys

In this tutorial, we showed how one could launch a contract using a Truffle migration workflow. However, what if we wanted to dynamically deploy any number of confidential contracts in our DApp? Instead of using truffle migrate, we can deploy confidential contracts on demand in much the same way it would be done with a web3 non-confidential contract.

If you view the application code here you'll see an example of a dynamically generated ballot.

Specifically, the following lines:

window.deploy = async function() {
  // ... (code omitted for space)

  // construct the Contract with the
  // 1) abi, 2) address (undefined on deploy) 3) default transaction options
  let ballot = new web3.oasis.Contract(ballot_artifacts.abi, undefined, {from: account});

  // make the deploy by passing in the bytecode and constructor arguments
  // invoke send() to actually make the transaction on chain
  ballotInstance = await ballot.deploy({
    data: ballot_artifacts.bytecode,
    arguments: [candidates]
  }).send();

  // ... (code omitted for space)
}

Note that the only change between a confidential and non-confidential deploy is the use of web3.oasis.Contract vs web3.eth.Contract. This holds more generally for most web3c.js APIs using the Contract object, including calls and sends on a Contract's ABI-defined methods.