Skip to content

Commit

Permalink
contracts, test: add Validate library
Browse files Browse the repository at this point in the history
  • Loading branch information
vinceau committed Sep 30, 2019
1 parent 4ffb3d2 commit bad009a
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 79 deletions.
47 changes: 2 additions & 45 deletions contracts/DarknodeSlasher/DarknodeSlasher.sol
@@ -1,9 +1,8 @@
pragma solidity ^0.5.8;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";
import "openzeppelin-solidity/contracts/cryptography/ECDSA.sol";

import "../libraries/String.sol";
import "../libraries/Validate.sol";
import "../DarknodeRegistry/DarknodeRegistry.sol";

/// @notice DarknodeSlasher will become a voting system for darknodes to
Expand Down Expand Up @@ -70,7 +69,7 @@ contract DarknodeSlasher is Ownable {
uint256 _validRound2,
bytes calldata _signature2
) external {
address signer = validateDuplicatePropose(
address signer = Validate.duplicatePropose(
_height,
_round,
_blockhash1,
Expand All @@ -84,46 +83,4 @@ contract DarknodeSlasher is Ownable {
slashed[_height][_round][signer] = true;
darknodeRegistry.slash(signer, msg.sender, maliciousSlashPercent);
}

function validateDuplicatePropose(
uint256 _height,
uint256 _round,
bytes memory _blockhash1,
uint256 _validRound1,
bytes memory _signature1,
bytes memory _blockhash2,
uint256 _validRound2,
bytes memory _signature2
) public pure returns (address) {
require(_validRound1 != _validRound2, "same valid round");
address signer1 = recoverPropose(_height, _round, _blockhash1, _validRound1, _signature1);
address signer2 = recoverPropose(_height, _round, _blockhash2, _validRound2, _signature2);
require(signer1 == signer2, "different signer");
return signer1;
}

function recoverPropose(
uint256 _height,
uint256 _round,
bytes memory _blockhash,
uint256 _validRound,
bytes memory _signature
) public pure returns (address) {
return ECDSA.recover(sha256(proposeMessage(_height, _round, _blockhash, _validRound)), _signature);
}

function proposeMessage(
uint256 _height,
uint256 _round,
bytes memory _blockhash,
uint256 _validRound
) public pure returns (bytes memory) {
return abi.encodePacked(
"Propose(Height=", String.fromUint(_height),
",Round=", String.fromUint(_round),
",BlockHash=", string(_blockhash),
",ValidRound=", String.fromUint(_validRound),
")"
);
}
}
55 changes: 55 additions & 0 deletions contracts/libraries/Validate.sol
@@ -0,0 +1,55 @@
pragma solidity ^0.5.8;

import "openzeppelin-solidity/contracts/cryptography/ECDSA.sol";

import "../libraries/String.sol";

/// @notice Validate is a library for validating malicious darknode behaviour.
library Validate {

/// @notice Recovers two propose messages and checks if they were signed by the same
/// darknode. If they were different but the height and round were the same,
/// then the darknode was behaving maliciously.
/// @return The address of the signer if and only if propose messages were different
function duplicatePropose(
uint256 _height,
uint256 _round,
bytes memory _blockhash1,
uint256 _validRound1,
bytes memory _signature1,
bytes memory _blockhash2,
uint256 _validRound2,
bytes memory _signature2
) internal pure returns (address) {
require(_validRound1 != _validRound2, "same valid round");
address signer1 = recoverPropose(_height, _round, _blockhash1, _validRound1, _signature1);
address signer2 = recoverPropose(_height, _round, _blockhash2, _validRound2, _signature2);
require(signer1 == signer2, "different signer");
return signer1;
}

function recoverPropose(
uint256 _height,
uint256 _round,
bytes memory _blockhash,
uint256 _validRound,
bytes memory _signature
) internal pure returns (address) {
return ECDSA.recover(sha256(proposeMessage(_height, _round, _blockhash, _validRound)), _signature);
}

function proposeMessage(
uint256 _height,
uint256 _round,
bytes memory _blockhash,
uint256 _validRound
) internal pure returns (bytes memory) {
return abi.encodePacked(
"Propose(Height=", String.fromUint(_height),
",Round=", String.fromUint(_round),
",BlockHash=", string(_blockhash),
",ValidRound=", String.fromUint(_validRound),
")"
);
}
}
63 changes: 63 additions & 0 deletions contracts/test/ValidateTest.sol
@@ -0,0 +1,63 @@
pragma solidity ^0.5.8;

import "../libraries/Validate.sol";

/// @notice Validate is a library for validating malicious darknode behaviour.
contract ValidateTest {

/// @notice Recovers two propose messages and checks if they were signed by the same
/// darknode. If they were different but the height and round were the same,
/// then the darknode was behaving maliciously.
/// @return The address of the signer if and only if propose messages were different
function duplicatePropose(
uint256 _height,
uint256 _round,
bytes memory _blockhash1,
uint256 _validRound1,
bytes memory _signature1,
bytes memory _blockhash2,
uint256 _validRound2,
bytes memory _signature2
) public pure returns (address) {
return Validate.duplicatePropose(
_height,
_round,
_blockhash1,
_validRound1,
_signature1,
_blockhash2,
_validRound2,
_signature2
);
}

function recoverPropose(
uint256 _height,
uint256 _round,
bytes memory _blockhash,
uint256 _validRound,
bytes memory _signature
) public pure returns (address) {
return Validate.recoverPropose(
_height,
_round,
_blockhash,
_validRound,
_signature
);
}

function proposeMessage(
uint256 _height,
uint256 _round,
bytes memory _blockhash,
uint256 _validRound
) public pure returns (bytes memory) {
return Validate.proposeMessage(
_height,
_round,
_blockhash,
_validRound
);
}
}
39 changes: 5 additions & 34 deletions test/DarknodeSlasher.ts
Expand Up @@ -20,7 +20,7 @@ const DarknodeRegistryStore = artifacts.require("DarknodeRegistryStore");
const DarknodeRegistry = artifacts.require("DarknodeRegistry");
const DarknodeSlasher = artifacts.require("DarknodeSlasher");

interface Darknode {
export interface Darknode {
account: Account;
privateKey: Buffer;
}
Expand Down Expand Up @@ -69,37 +69,8 @@ contract("DarknodeSlasher", (accounts: string[]) => {
await waitForEpoch(dnr);
});

describe("when generating messages", async () => {

it("should correctly generate the propose message", async () => {
const height = new BN("6349374925919561232");
const round = new BN("3652381888914236532");
const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
const hexBlockhash = web3.utils.asciiToHex(blockhash);
const validRound = new BN("6345888412984379713");
const proposeMsg = generateProposeMessage(height, round, blockhash, validRound);
const rawMsg = await slasher.proposeMessage(height, round, hexBlockhash, validRound);
proposeMsg.should.be.equal(web3.utils.hexToAscii(rawMsg));
});
});

describe("when handling propose messages", async () => {

it("should recover the signer of a message", async () => {
const darknode = darknodes[0];
const height = new BN("6349374925919561232");
const round = new BN("3652381888914236532");
const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
const hexBlockhash = web3.utils.asciiToHex(blockhash);
const validRound = new BN("6345888412984379713");
const proposeMsg = generateProposeMessage(height, round, blockhash, validRound);
const hash = hashjs.sha256().update(proposeMsg).digest('hex')
const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
const sigString = Ox(`${sig.r.toString("hex")}${sig.s.toString("hex")}${(sig.v).toString(16)}`);
const signer = await slasher.recoverPropose(height, round, hexBlockhash, validRound, sigString);
signer.should.equal(darknode.account.address);
});

it("cannot slash when the same data is given twice", async () => {
const darknode = darknodes[0];
const height = new BN("6349374925919561232");
Expand Down Expand Up @@ -229,8 +200,8 @@ contract("DarknodeSlasher", (accounts: string[]) => {

// });

const generateProposeMessage = (height: BN, round: BN, blockHash: string, validRound: BN): string => {
return `Propose(Height=${height.toString()},Round=${round.toString()},BlockHash=${blockHash},ValidRound=${validRound.toString()})`;
}

});

export const generateProposeMessage = (height: BN, round: BN, blockHash: string, validRound: BN): string => {
return `Propose(Height=${height.toString()},Round=${round.toString()},BlockHash=${blockHash},ValidRound=${validRound.toString()})`;
}
65 changes: 65 additions & 0 deletions test/Validate.ts
@@ -0,0 +1,65 @@
import BN from "bn.js";

import hashjs from 'hash.js';
import { ecsign } from "ethereumjs-util";
import { Ox } from "./helper/testUtils";

import { ValidateTestInstance } from "../types/truffle-contracts";
import { Darknode, generateProposeMessage } from "./DarknodeSlasher";

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

const numDarknodes = 2;

contract("Validate", (accounts: string[]) => {

let validateList: ValidateTestInstance;
let darknodes = new Array<Darknode>();

before(async () => {
validateList = await ValidateTest.new();

for (let i = 0; i < numDarknodes; i++) {
const darknode = web3.eth.accounts.create();
const privKey = Buffer.from(darknode.privateKey.slice(2), "hex");
darknodes.push({
account: darknode,
privateKey: privKey,
});
}
});

describe("when generating messages", async () => {

it("should correctly generate the propose message", async () => {
const height = new BN("6349374925919561232");
const round = new BN("3652381888914236532");
const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
const hexBlockhash = web3.utils.asciiToHex(blockhash);
const validRound = new BN("6345888412984379713");
const proposeMsg = generateProposeMessage(height, round, blockhash, validRound);
const rawMsg = await validateList.proposeMessage(height, round, hexBlockhash, validRound);
proposeMsg.should.be.equal(web3.utils.hexToAscii(rawMsg));
});
});

describe("when handling propose messages", async () => {

it("should recover the signer of a message", async () => {
const darknode = darknodes[0];
const height = new BN("6349374925919561232");
const round = new BN("3652381888914236532");
const blockhash = "XTsJ2rO2yD47tg3JfmakVRXLzeou4SMtZvsMc6lkr6o";
const hexBlockhash = web3.utils.asciiToHex(blockhash);
const validRound = new BN("6345888412984379713");
const proposeMsg = generateProposeMessage(height, round, blockhash, validRound);
const hash = hashjs.sha256().update(proposeMsg).digest('hex')
const sig = ecsign(Buffer.from(hash, "hex"), darknode.privateKey);
const sigString = Ox(`${sig.r.toString("hex")}${sig.s.toString("hex")}${(sig.v).toString(16)}`);
const signer = await validateList.recoverPropose(height, round, hexBlockhash, validRound, sigString);
signer.should.equal(darknode.account.address);
});

});

});

0 comments on commit bad009a

Please sign in to comment.