diff --git a/packages/protocol/contracts/identity/Random.sol b/packages/protocol/contracts/identity/Random.sol index 56badc66807..cdc9e4afca6 100644 --- a/packages/protocol/contracts/identity/Random.sol +++ b/packages/protocol/contracts/identity/Random.sol @@ -1,19 +1,47 @@ pragma solidity ^0.5.3; import "./interfaces/IRandom.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; +import "../common/Initializable.sol"; /** * @title Provides randomness for verifier selection */ -contract Random is IRandom { +contract Random is IRandom, Ownable, Initializable { + + using SafeMath for uint256; /* Stores most recent commitment per address */ mapping(address => bytes32) public commitments; - bytes32 public _random; + uint256 public randomnessBlockRetentionWindow = 256; + + mapping (uint256 => bytes32) private history; + uint256 private historyFirst; + uint256 private historySize; + + event RandomnessBlockRetentionWindowSet(uint256 value); - function initialize() external { + /** + * @notice Initializes the contract with initial parameters. + * @param _randomnessBlockRetentionWindow Number of old random blocks whose randomness + * values can be queried. + */ + function initialize(uint256 _randomnessBlockRetentionWindow) external initializer { + _transferOwnership(msg.sender); + setRandomnessBlockRetentionWindow(_randomnessBlockRetentionWindow); + } + + /** + * @notice Sets the number of old random blocks whose randomness values can be queried. + * @param value Number of old random blocks whose randomness values can be queried. + */ + function setRandomnessBlockRetentionWindow(uint256 value) public onlyOwner { + require(value > 0, "randomnessBlockRetetionWindow cannot be zero"); + randomnessBlockRetentionWindow = value; + emit RandomnessBlockRetentionWindowSet(value); } /** @@ -31,28 +59,104 @@ contract Random is IRandom { bytes32 newCommitment, address proposer ) external { - require(msg.sender == address(0)); + require(msg.sender == address(0), "only VM can call"); + _revealAndCommit(randomness, newCommitment, proposer); + } + /** + * @notice Implements step of the randomness protocol. + * @param randomness Bytes that will be added to the entropy pool. + * @param newCommitment The hash of randomness that will be revealed in the future. + * @param proposer Address of the block proposer. + */ + function _revealAndCommit( + bytes32 randomness, + bytes32 newCommitment, + address proposer + ) internal { // ensure revealed randomness matches previous commitment if (commitments[proposer] != 0) { - require(randomness != 0); + require(randomness != 0, "randomness cannot be zero if there is a previous commitment"); bytes32 expectedCommitment = computeCommitment(randomness); - require(expectedCommitment == commitments[proposer]); + require( + expectedCommitment == commitments[proposer], + "commitment didn't match the posted randomness" + ); } else { - require(randomness == 0); + require(randomness == 0, "randomness should be zero if there is no previous commitment"); } // add entropy - _random = keccak256(abi.encodePacked(_random, randomness)); + uint256 blockNumber = block.number == 0 ? 0 : block.number.sub(1); + addRandomness(block.number, keccak256(abi.encodePacked(history[blockNumber], randomness))); commitments[proposer] = newCommitment; } + /** + * @notice Add a value to the randomness history. + * @param blockNumber Current block number. + * @param randomness The new randomness added to history. + * @dev The calls to this function should be made so that on the next call, blockNumber will + * be the previous one, incremented by one. + */ + function addRandomness(uint256 blockNumber, bytes32 randomness) internal { + history[blockNumber] = randomness; + if (historySize == 0) { + historyFirst = block.number; + historySize = 1; + } else if (historySize > randomnessBlockRetentionWindow) { + delete history[historyFirst]; + delete history[historyFirst+1]; + historyFirst += 2; + historySize--; + } else if (historySize == randomnessBlockRetentionWindow) { + delete history[historyFirst]; + historyFirst++; + } else /* historySize < randomnessBlockRetentionWindow */ { + historySize++; + } + } + + /** + * @notice Compute the commitment hash for a given randomness value. + * @param randomness The value for which the commitment hash is computed. + * @return Commitment parameter. + */ function computeCommitment(bytes32 randomness) public pure returns (bytes32) { return keccak256(abi.encodePacked(randomness)); } + /** + * @notice Querying the current randomness value. + * @return Returns the current randomness value. + */ function random() external view returns (bytes32) { - return _random; + return _getBlockRandomness(block.number, block.number); + } + + /** + * @notice Get randomness values of previous blocks. + * @param blockNumber The number of block whose randomness value we want to know. + * @return The associated randomness value. + */ + function getBlockRandomness(uint256 blockNumber) external view returns (bytes32) { + return _getBlockRandomness(blockNumber, block.number); + } + + /** + * @notice Get randomness values of previous blocks. + * @param blockNumber The number of block whose randomness value we want to know. + * @param cur Number of the current block. + * @return The associated randomness value. + */ + function _getBlockRandomness(uint256 blockNumber, uint256 cur) internal view returns (bytes32) { + require(blockNumber <= cur, "Cannot query randomness of future blocks"); + require( + blockNumber > cur.sub(historySize) && + (randomnessBlockRetentionWindow >= cur || + blockNumber > cur.sub(randomnessBlockRetentionWindow)), + "Cannot query randomness older than the stored history"); + return history[blockNumber]; } } diff --git a/packages/protocol/contracts/identity/test/TestRandom.sol b/packages/protocol/contracts/identity/test/TestRandom.sol new file mode 100644 index 00000000000..8930cc9e145 --- /dev/null +++ b/packages/protocol/contracts/identity/test/TestRandom.sol @@ -0,0 +1,13 @@ +pragma solidity ^0.5.3; + +import "../Random.sol"; + +contract TestRandom is Random { + function addTestRandomness(uint256 blockNumber, bytes32 randomness) external { + addRandomness(blockNumber, randomness); + } + function getTestRandomness(uint256 blockNumber, uint256 cur) external view returns (bytes32) { + return _getBlockRandomness(blockNumber, cur); + } +} + diff --git a/packages/protocol/migrations/13_random.ts b/packages/protocol/migrations/13_random.ts index a1ef4117110..0069a349c9a 100644 --- a/packages/protocol/migrations/13_random.ts +++ b/packages/protocol/migrations/13_random.ts @@ -1,5 +1,15 @@ import { CeloContractName } from '@celo/protocol/lib/registry-utils' import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils' +import { config } from '@celo/protocol/migrationsConfig' import { RandomInstance } from 'types' -module.exports = deploymentForCoreContract(web3, artifacts, CeloContractName.Random) +const initializeArgs = async (_: string): Promise => { + return [config.random.randomnessBlockRetentionWindow] +} + +module.exports = deploymentForCoreContract( + web3, + artifacts, + CeloContractName.Random, + initializeArgs +) diff --git a/packages/protocol/migrations/16_governance.ts b/packages/protocol/migrations/16_governance.ts index 6c5430da86d..c9ce18f18e5 100644 --- a/packages/protocol/migrations/16_governance.ts +++ b/packages/protocol/migrations/16_governance.ts @@ -44,7 +44,7 @@ module.exports = deploymentForCoreContract( ) await reserve.addSpender(governance.address) - const proxyOwnedByGovernance = ['GoldToken', 'Random'] + const proxyOwnedByGovernance = ['GoldToken'] await Promise.all( proxyOwnedByGovernance.map((contractName) => transferOwnershipOfProxy(contractName, governance.address, artifacts) @@ -61,6 +61,7 @@ module.exports = deploymentForCoreContract( 'GasPriceMinimum', 'Governance', 'LockedGold', + 'Random', 'Registry', 'Reserve', 'SortedOracles', diff --git a/packages/protocol/migrationsConfig.js b/packages/protocol/migrationsConfig.js index a710ab180c6..c5747fa40b3 100644 --- a/packages/protocol/migrationsConfig.js +++ b/packages/protocol/migrationsConfig.js @@ -11,11 +11,12 @@ const DefaultConfig = { attestationExpirySeconds: 60 * 60, // 1 hour, attestationRequestFeeInDollars: 0.05, }, - lockedGold: { - unlockingPeriod: 60 * 60 * 24 * 3, // 3 days - }, - oracles: { - reportExpiry: 60 * 60, // 1 hour + blockchainParameters: { + minimumClientVersion: { + major: 1, + minor: 8, + patch: 23, + }, }, election: { minElectableValidators: '22', @@ -29,6 +30,12 @@ const DefaultConfig = { updateFrequency: 3600, minimumReports: 1, }, + gasPriceMinimum: { + initialMinimum: 10000, + targetDensity: 1 / 2, + adjustmentSpeed: 1 / 2, + proposerFraction: 1 / 2, + }, governance: { approvalStageDuration: 15 * 60, // 15 minutes concurrentProposals: 10, @@ -42,11 +49,14 @@ const DefaultConfig = { participationBaselineUpdateFactor: 1 / 5, participationBaselineQuorumFactor: 1, }, - gasPriceMinimum: { - initialMinimum: 10000, - targetDensity: 1 / 2, - adjustmentSpeed: 1 / 2, - proposerFraction: 1 / 2, + lockedGold: { + unlockingPeriod: 60 * 60 * 24 * 3, // 3 days + }, + oracles: { + reportExpiry: 60 * 60, // 1 hour + }, + random: { + randomnessBlockRetentionWindow: 256, }, registry: { predeployedProxyAddress: '0x000000000000000000000000000000000000ce10', @@ -91,13 +101,6 @@ const DefaultConfig = { groupName: 'C-Labs', commission: 0.1, }, - blockchainParameters: { - minimumClientVersion: { - major: 1, - minor: 8, - patch: 23, - }, - }, } const linkedLibraries = { diff --git a/packages/protocol/test/identity/attestations.ts b/packages/protocol/test/identity/attestations.ts index 3e837e71d8a..79bfb042ccf 100644 --- a/packages/protocol/test/identity/attestations.ts +++ b/packages/protocol/test/identity/attestations.ts @@ -19,10 +19,10 @@ import { MockLockedGoldInstance, MockStableTokenContract, MockStableTokenInstance, - MockElectionContract, MockElectionInstance, - RandomContract, - RandomInstance, + TestRandomContract, + TestRandomInstance, + MockElectionContract, RegistryContract, RegistryInstance, } from 'types' @@ -37,7 +37,7 @@ const Attestations: TestAttestationsContract = artifacts.require('TestAttestatio const MockStableToken: MockStableTokenContract = artifacts.require('MockStableToken') const MockElection: MockElectionContract = artifacts.require('MockElection') const MockLockedGold: MockLockedGoldContract = artifacts.require('MockLockedGold') -const Random: RandomContract = artifacts.require('Random') +const Random: TestRandomContract = artifacts.require('TestRandom') const Registry: RegistryContract = artifacts.require('Registry') const dataEncryptionKey = '0x02f2f48ee19680706196e2e339e5da3491186e0c4c5030670656b0e01611111111' @@ -49,7 +49,7 @@ contract('Attestations', (accounts: string[]) => { let attestations: TestAttestationsInstance let mockStableToken: MockStableTokenInstance let otherMockStableToken: MockStableTokenInstance - let random: RandomInstance + let random: TestRandomInstance let mockElection: MockElectionInstance let mockLockedGold: MockLockedGoldInstance let registry: RegistryInstance @@ -142,6 +142,7 @@ contract('Attestations', (accounts: string[]) => { otherMockStableToken = await MockStableToken.new() attestations = await Attestations.new() random = await Random.new() + random.addTestRandomness(0, '0x00') mockLockedGold = await MockLockedGold.new() await Promise.all( accounts.map((account) => diff --git a/packages/protocol/test/identity/random.ts b/packages/protocol/test/identity/random.ts new file mode 100644 index 00000000000..2ffca631849 --- /dev/null +++ b/packages/protocol/test/identity/random.ts @@ -0,0 +1,105 @@ +import { assertRevert, assertEqualBN, assertContainSubset } from '@celo/protocol/lib/test-utils' + +import { TestRandomContract, TestRandomInstance } from 'types' +import { BigNumber } from 'bignumber.js' + +const Random: TestRandomContract = artifacts.require('TestRandom') + +// @ts-ignore +// TODO(mcortesi): Use BN +Random.numberFormat = 'BigNumber' + +contract('Random', (accounts: string[]) => { + let random: TestRandomInstance + + beforeEach(async () => { + random = await Random.new() + random.initialize(256) + }) + + describe('#setRandomnessRetentionWindow()', () => { + it('should set the variable', async () => { + await random.setRandomnessBlockRetentionWindow(1000) + assertEqualBN(new BigNumber(1000), await random.randomnessBlockRetentionWindow()) + }) + + it('should emit the event', async () => { + const response = await random.setRandomnessBlockRetentionWindow(1000) + assert.equal(response.logs.length, 1) + const log = response.logs[0] + assertContainSubset(log, { + event: 'RandomnessBlockRetentionWindowSet', + args: { + value: new BigNumber(1000), + }, + }) + }) + + it('only owner can set', async () => { + assertRevert(random.setRandomnessBlockRetentionWindow(1000, { from: accounts[1] })) + }) + }) + + describe('#addTestRandomness', () => { + const randomValues = [ + '0x0000000000000000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000002', + '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000004', + '0x0000000000000000000000000000000000000000000000000000000000000005', + '0x0000000000000000000000000000000000000000000000000000000000000006', + '0x0000000000000000000000000000000000000000000000000000000000000007', + ] + it('should be able to simulate adding randomness', async () => { + await random.addTestRandomness(1, randomValues[1]) + await random.addTestRandomness(2, randomValues[2]) + await random.addTestRandomness(3, randomValues[3]) + await random.addTestRandomness(4, randomValues[4]) + assert.equal(randomValues[1], await random.getTestRandomness(1, 4)) + assert.equal(randomValues[2], await random.getTestRandomness(2, 4)) + assert.equal(randomValues[3], await random.getTestRandomness(3, 4)) + assert.equal(randomValues[4], await random.getTestRandomness(4, 4)) + }) + + describe('when changing history smaller', () => { + beforeEach(async () => { + await random.addTestRandomness(1, randomValues[1]) + await random.addTestRandomness(2, randomValues[2]) + await random.addTestRandomness(3, randomValues[3]) + await random.addTestRandomness(4, randomValues[4]) + await random.setRandomnessBlockRetentionWindow(2) + }) + it('can still add randomness', async () => { + await random.addTestRandomness(5, randomValues[5]) + assert.equal(randomValues[5], await random.getTestRandomness(5, 5)) + }) + it('cannot read old blocks', async () => { + assertRevert(random.getTestRandomness(3, 5)) + }) + }) + + describe('when changing history larger', () => { + beforeEach(async () => { + await random.setRandomnessBlockRetentionWindow(2) + await random.addTestRandomness(1, randomValues[1]) + await random.addTestRandomness(2, randomValues[2]) + await random.addTestRandomness(3, randomValues[3]) + await random.addTestRandomness(4, randomValues[4]) + await random.setRandomnessBlockRetentionWindow(4) + }) + it('can still add randomness', async () => { + await random.addTestRandomness(5, randomValues[5]) + assert.equal(randomValues[5], await random.getTestRandomness(5, 5)) + }) + it('cannot read old blocks', async () => { + assertRevert(random.getTestRandomness(1, 5)) + }) + it('old values are preserved', async () => { + await random.addTestRandomness(5, randomValues[5]) + await random.addTestRandomness(6, randomValues[6]) + assert.equal(randomValues[3], await random.getTestRandomness(3, 6)) + }) + }) + }) +}) diff --git a/packages/protocol/test/stability/stabletoken.ts b/packages/protocol/test/stability/stabletoken.ts index cc6636e543c..722b5aae445 100644 --- a/packages/protocol/test/stability/stabletoken.ts +++ b/packages/protocol/test/stability/stabletoken.ts @@ -29,7 +29,7 @@ contract('StableToken', (accounts: string[]) => { beforeEach(async () => { registry = await Registry.new() stableToken = await StableToken.new() - await stableToken.initialize( + const response = await stableToken.initialize( 'Celo Dollar', 'cUSD', 18, @@ -39,7 +39,7 @@ contract('StableToken', (accounts: string[]) => { [], [] ) - initializationTime = (await web3.eth.getBlock('latest')).timestamp + initializationTime = (await web3.eth.getBlock(response.receipt.blockNumber)).timestamp }) describe('#initialize()', () => {