Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
contracts, test: add Validate library
- Loading branch information
Showing
5 changed files
with
190 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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), | ||
")" | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
|
||
}); | ||
|
||
}); |