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

Add basic implementation to commit votes #393

Merged
merged 3 commits into from
May 8, 2019
Merged
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
41 changes: 41 additions & 0 deletions v1/contracts/Voting.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
pragma solidity ^0.5.0;


/**
* @title Voting system for Oracle.
* @dev Handles receiving and resolving price requests via a commit-reveal voting scheme.
*/
contract Voting {

struct VoteInstance {
// Maps (voterAddress) to their committed hash.
// A bytes32 of `0` indicates no commit or a commit that was already revealed.
mapping(address => bytes32) committedHashes;
}

// Conceptually maps (identifier, time) to a `VoteInstance`.
mapping(bytes32 => mapping(uint => VoteInstance)) private requests;

/**
* @notice Commit your vote for a price request for `identifier` at `time`.
* @dev (`identifier`, `time`) must correspond to a price request that's currently in the commit phase. `hash`
* should be the keccak256 hash of the price you want to vote for and a `int salt`. Commits can be changed.
*/
function commitVote(bytes32 identifier, uint time, bytes32 hash) external {
require(hash != bytes32(0), "Committed hash of 0 is disallowed, choose a different salt");
VoteInstance storage voteInstance = requests[identifier][time];
voteInstance.committedHashes[msg.sender] = hash;
}

/**
* @notice Reveal a previously committed vote for `identifier` at `time`.
* @dev The revealed `price` and `salt` must match the latest `hash` that `commitVote()` was called with. Only the
* committer can reveal their vote.
*/
function revealVote(bytes32 identifier, uint time, int price, int salt) external {
VoteInstance storage voteInstance = requests[identifier][time];
require(keccak256(abi.encode(price, salt)) == voteInstance.committedHashes[msg.sender],
"Committed hash doesn't match revealed price and salt");
delete voteInstance.committedHashes[msg.sender];
}
}
131 changes: 131 additions & 0 deletions v1/test/Voting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
const { didContractThrow } = require("../../common/SolidityTestUtils.js");

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

contract("Voting", function(accounts) {
const account1 = accounts[0];
const account2 = accounts[1];
const account3 = accounts[2];
const account4 = accounts[3];

const getRandomInt = () => {
return web3.utils.toBN(web3.utils.randomHex(32));
};

it("One voter, one request", async function() {
const voting = await Voting.new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add this contract to the migrations and use .deployed(). We can do so in a later PR, but just wanted to note that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that in a follow up PR.


const identifier = web3.utils.utf8ToHex("id");
const time = "1000";

const price = getRandomInt();
const salt = getRandomInt();
const hash = web3.utils.soliditySha3(price, salt);

// Can't commit hash of 0.
assert(await didContractThrow(voting.commitVote(identifier, time, "0x0")));

// Can't reveal before committing.
assert(await didContractThrow(voting.revealVote(identifier, time, price, salt)));
// Can commit a new hash.
await voting.commitVote(identifier, time, hash);

// Voters can alter their commits.
const newPrice = getRandomInt();
const newSalt = getRandomInt();
const newHash = web3.utils.soliditySha3(newPrice, newSalt);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know this function existed - nice!


// Can't reveal before committing.
assert(await didContractThrow(voting.revealVote(identifier, time, newPrice, newSalt)));
// Can alter a committed hash.
await voting.commitVote(identifier, time, newHash);

// Can't reveal the overwritten commit.
assert(await didContractThrow(voting.revealVote(identifier, time, price, salt)));

// Can't reveal with the wrong price but right salt, and reverse.
assert(await didContractThrow(voting.revealVote(identifier, time, newPrice, salt)));
assert(await didContractThrow(voting.revealVote(identifier, time, price, newSalt)));

// Successfully reveal the latest commit.
await voting.revealVote(identifier, time, newPrice, newSalt);

// Can't reveal the same commit again.
assert(await didContractThrow(voting.revealVote(identifier, time, newPrice, newSalt)));
});

it("Multiple voters", async function() {
const voting = await Voting.new();

const identifier = web3.utils.utf8ToHex("id");
const time = "1000";

const price1 = getRandomInt();
const salt1 = getRandomInt();
const hash1 = web3.utils.soliditySha3(price1, salt1);

const price2 = getRandomInt();
const salt2 = getRandomInt();
const hash2 = web3.utils.soliditySha3(price2, salt2);

// Voter3 wants to vote the same price as voter1.
const price3 = price1;
const salt3 = getRandomInt();
const hash3 = web3.utils.soliditySha3(price3, salt3);

// Multiple voters can commit.
await voting.commitVote(identifier, time, hash1, { from: account1 });
await voting.commitVote(identifier, time, hash2, { from: account2 });
await voting.commitVote(identifier, time, hash3, { from: account3 });

// They can't reveal each other's votes.
assert(await didContractThrow(voting.revealVote(identifier, time, price2, salt2, { from: account1 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price3, salt3, { from: account1 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price1, salt1, { from: account2 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price3, salt3, { from: account2 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price1, salt1, { from: account3 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price2, salt2, { from: account3 })));

// Someone who didn't even commit can't reveal anything either.
assert(await didContractThrow(voting.revealVote(identifier, time, price1, salt1, { from: account4 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price2, salt2, { from: account4 })));
assert(await didContractThrow(voting.revealVote(identifier, time, price3, salt3, { from: account4 })));

// They can reveal their own votes.
await voting.revealVote(identifier, time, price1, salt1, { from: account1 });
await voting.revealVote(identifier, time, price2, salt2, { from: account2 });
await voting.revealVote(identifier, time, price3, salt3, { from: account3 });
});

it("Overlapping request keys", async function() {
const voting = await Voting.new();

// Verify that concurrent votes with the same identifier but different times, or the same time but different
// identifiers don't cause any problems.
const identifier1 = web3.utils.utf8ToHex("id1");
const time1 = "1000";
const identifier2 = web3.utils.utf8ToHex("id2");
const time2 = "2000";

const price1 = getRandomInt();
const salt1 = getRandomInt();
const hash1 = web3.utils.soliditySha3(price1, salt1);

const price2 = getRandomInt();
const salt2 = getRandomInt();
const hash2 = web3.utils.soliditySha3(price2, salt2);

await voting.commitVote(identifier1, time2, hash1);
await voting.commitVote(identifier2, time1, hash2);

// Can't reveal the wrong combos.
assert(await didContractThrow(voting.revealVote(identifier1, time2, price2, salt2)));
assert(await didContractThrow(voting.revealVote(identifier2, time1, price1, salt1)));
assert(await didContractThrow(voting.revealVote(identifier1, time1, price1, salt1)));
assert(await didContractThrow(voting.revealVote(identifier1, time1, price2, salt2)));

// Can reveal the right combos.
voting.revealVote(identifier1, time2, price1, salt1);
voting.revealVote(identifier2, time1, price2, salt2);
});
});