diff --git a/contracts/reputationMiningCycle/ReputationMiningCycleCommon.sol b/contracts/reputationMiningCycle/ReputationMiningCycleCommon.sol index 5262aa2882..ab324f33fb 100644 --- a/contracts/reputationMiningCycle/ReputationMiningCycleCommon.sol +++ b/contracts/reputationMiningCycle/ReputationMiningCycleCommon.sol @@ -162,7 +162,7 @@ contract ReputationMiningCycleCommon is ReputationMiningCycleStorage, PatriciaTr return false; } uint256 target = windowOpenFor * Y; - if (uint256(keccak256(abi.encodePacked(minerAddress, _stage))) > target) { + if (uint256(keccak256(abi.encodePacked(minerAddress, address(this), _stage))) > target) { return false; } } diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 958ec9803e..ae41ac4a41 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -297,6 +297,18 @@ exports.currentBlock = async function currentBlock() { return p; }; +exports.getBlock = async function getBlock(blockNumber) { + const p = new Promise((resolve, reject) => { + web3.eth.getBlock(blockNumber, (err, res) => { + if (err) { + return reject(err); + } + return resolve(res); + }); + }); + return p; +}; + exports.getBlockTime = async function getBlockTime(blockNumber = "latest") { const p = new Promise((resolve, reject) => { web3.eth.getBlock(blockNumber, (err, res) => { @@ -1032,8 +1044,12 @@ exports.getMiningCycleCompletePromise = async function getMiningCycleCompletePro colonyNetworkEthers.on("ReputationMiningCycleComplete", async (_hash, _nLeaves, event) => { const colonyNetwork = await IColonyNetwork.at(colonyNetworkEthers.address); const newHash = await colonyNetwork.getReputationRootHash(); - expect(newHash).to.not.equal(oldHash, "The old and new hashes are the same"); - expect(newHash).to.equal(expectedHash, "The network root hash doens't match the one submitted"); + if (oldHash) { + expect(newHash).to.not.equal(oldHash, "The old and new hashes are the same"); + } + if (expectedHash) { + expect(newHash).to.equal(expectedHash, "The network root hash doesn't match the one submitted"); + } event.removeListener(); resolve(); }); @@ -1104,6 +1120,12 @@ exports.rolesToBytes32 = function rolesToBytes32(roles) { return `0x${new BN(roles.map((role) => new BN(1).shln(role)).reduce((a, b) => a.or(b), new BN(0))).toString(16, 64)}`; }; +exports.sleep = function sleep(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + class TestAdapter { constructor() { this.outputs = []; diff --git a/packages/metatransaction-broadcaster/Dockerfile b/packages/metatransaction-broadcaster/Dockerfile index b00e1b679a..5b767f1a46 100644 --- a/packages/metatransaction-broadcaster/Dockerfile +++ b/packages/metatransaction-broadcaster/Dockerfile @@ -4,5 +4,7 @@ COPY ./package.json ./ COPY ./package-lock.json ./ COPY ./build ./build RUN npm ci +RUN cd ./packages/metatransaction-broadcaster/ && npm i +RUN cd ./packages/package-utils/ && npm i EXPOSE 3000 CMD node $NODE_ARGS packages/metatransaction-broadcaster/bin/index.js --colonyNetworkAddress $COLONYNETWORK_ADDRESS --privateKey $PRIVATE_KEY --gasLimit $GASLIMIT $ARGS diff --git a/packages/reputation-miner/Dockerfile b/packages/reputation-miner/Dockerfile index 2203791963..1ab9630f07 100644 --- a/packages/reputation-miner/Dockerfile +++ b/packages/reputation-miner/Dockerfile @@ -1,8 +1,11 @@ FROM node:14-bullseye +RUN apt-get update || : && apt-get install python -y COPY ./packages ./packages COPY ./package.json ./ COPY ./package-lock.json ./ COPY ./build ./build RUN npm install +RUN cd ./packages/reputation-miner/ && npm i +RUN cd ./packages/package-utils/ && npm i EXPOSE 3000 CMD node $NODE_ARGS packages/reputation-miner/bin/index.js --dbPath $REPUTATION_JSON_PATH --colonyNetworkAddress $COLONYNETWORK_ADDRESS --privateKey $PRIVATE_KEY --syncFrom $SYNC_FROM_BLOCK $ARGS diff --git a/packages/reputation-miner/ReputationMiner.js b/packages/reputation-miner/ReputationMiner.js index 147926e1c4..ec76812b68 100644 --- a/packages/reputation-miner/ReputationMiner.js +++ b/packages/reputation-miner/ReputationMiner.js @@ -753,11 +753,13 @@ class ReputationMiner { if (!entryIndex) { entryIndex = await this.getEntryIndex(); // eslint-disable-line no-param-reassign } - let gasEstimate = ethers.BigNumber.from(1000000); + let gasEstimate; try { - gasEstimate = await repCycle.estimate.submitRootHash(hash, nLeaves, jrh, entryIndex); - } catch (err) { // eslint-disable-line no-empty - + gasEstimate = await repCycle.estimateGas.submitRootHash(hash, nLeaves, jrh, entryIndex); + // Add some extra gas just in case the details change and a little more is needed + gasEstimate = gasEstimate.mul(11).div(10); + } catch (err) { + gasEstimate = ethers.BigNumber.from(1000000); } // Submit that entry @@ -1001,11 +1003,13 @@ class ReputationMiner { const [, siblings2] = await this.justificationTree.getProof(ReputationMiner.getHexString(totalnUpdates, 64)); const [round, index] = await this.getMySubmissionRoundAndIndex(); - let gasEstimate = ethers.BigNumber.from(6000000); + let gasEstimate; try { - gasEstimate = await repCycle.estimate.confirmJustificationRootHash(round, index, siblings1, siblings2); - } catch (err) { // eslint-disable-line no-empty - + gasEstimate = await repCycle.estimateGas.confirmJustificationRootHash(round, index, siblings1, siblings2); + // Add some extra gas just in case the details change and a little more is needed + gasEstimate = gasEstimate.mul(11).div(10) + } catch (err) { + gasEstimate = ethers.BigNumber.from(6000000); } return repCycle.confirmJustificationRootHash( @@ -1083,16 +1087,18 @@ class ReputationMiner { ); } - let gasEstimate = ethers.BigNumber.from(1000000); + let gasEstimate; try { - gasEstimate = await repCycle.estimate.respondToBinarySearchForChallenge( + gasEstimate = await repCycle.estimateGas.respondToBinarySearchForChallenge( round, index, intermediateReputationHash, siblings ); - } catch (err) { // eslint-disable-line no-empty - + // Add some extra gas just in case the details change and a little more is needed + gasEstimate = gasEstimate.mul(11).div(10); + } catch (err) { + gasEstimate = ethers.BigNumber.from(1000000); } return repCycle.respondToBinarySearchForChallenge( round, @@ -1121,12 +1127,14 @@ class ReputationMiner { const intermediateReputationHash = this.justificationHashes[targetLeafKeyAsHex].jhLeafValue; const [, siblings] = await this.justificationTree.getProof(targetLeafKeyAsHex); - let gasEstimate = ethers.BigNumber.from(1000000); + let gasEstimate try { - gasEstimate = await repCycle.estimate.confirmBinarySearchResult(round, index, intermediateReputationHash, siblings); - } catch (err){ // eslint-disable-line no-empty - + gasEstimate = await repCycle.estimateGas.confirmBinarySearchResult(round, index, intermediateReputationHash, siblings); + // Add some extra gas just in case the details change and a little more is needed + gasEstimate = gasEstimate.mul(11).div(10); + } catch (err){ + gasEstimate = ethers.BigNumber.from(1000000); } return repCycle.confirmBinarySearchResult(round, index, intermediateReputationHash, siblings, { @@ -1225,11 +1233,13 @@ class ReputationMiner { lastAgreeJustifications.childReputationProof.siblings, lastAgreeJustifications.adjacentReputationProof.siblings] - let gasEstimate = ethers.BigNumber.from(4000000); + let gasEstimate; try { - gasEstimate = await repCycle.estimate.respondToChallenge(...functionArgs); - } catch (err){ // eslint-disable-line no-empty - + gasEstimate = await repCycle.estimateGas.respondToChallenge(...functionArgs); + // Add some extra gas just in case the details change and a little more is needed + gasEstimate = gasEstimate.mul(11).div(10); + } catch (err){ + gasEstimate = ethers.BigNumber.from(4000000); } return repCycle.respondToChallenge(...functionArgs, @@ -1245,11 +1255,13 @@ class ReputationMiner { const repCycle = await this.getActiveRepCycle(); const [round] = await this.getMySubmissionRoundAndIndex(); - let gasEstimate = ethers.BigNumber.from(4000000); + let gasEstimate; try { gasEstimate = await repCycle.estimateGas.confirmNewHash(round); - } catch (err){ // eslint-disable-line no-empty - + // Add some extra gas just in case the details change and a little more is needed + gasEstimate = gasEstimate.mul(11).div(10); + } catch (err){ + gasEstimate = ethers.BigNumber.from(4000000); } return repCycle.confirmNewHash(round, { gasLimit: gasEstimate, gasPrice: this.gasPrice }); } diff --git a/packages/reputation-miner/ReputationMinerClient.js b/packages/reputation-miner/ReputationMinerClient.js index fa103e030a..c8c44a9063 100644 --- a/packages/reputation-miner/ReputationMinerClient.js +++ b/packages/reputation-miner/ReputationMinerClient.js @@ -21,6 +21,16 @@ const CHALLENGE_RESPONSE_WINDOW_DURATION = 20 * 60; const cache = apicache.middleware +const racingFunctionSignatures = [ + "submitRootHash(bytes32,uint256,bytes32,uint256)", + "confirmNewHash(uint256)", + "invalidateHash(uint256,uint256)", + "respondToBinarySearchForChallenge(uint256,uint256,bytes,bytes32[])", + "confirmBinarySearchResult(uint256,uint256,bytes,bytes32[])", + "respondToChallenge(uint256[26],bytes32[7],bytes32[],bytes32[],bytes32[],bytes32[],bytes32[],bytes32[])", + "confirmJustificationRootHash(uint256,uint256,bytes32[],bytes32[])" +].map(x => ethers.utils.id(x).slice(0,10)) + class ReputationMinerClient { /** * Constructor for ReputationMiner @@ -607,6 +617,13 @@ class ReputationMinerClient { return; } this._adapter.error(`Error during block checks: ${err}`); + if (racingFunctionSignatures.indexOf(err.transaction.data.slice(0, 10)) > -1){ + // An error on a function that we were 'racing' to execute failed - most likely because someone else did it. + // So let's keep mining. + console.log('Sometimes-expected transaction failure - we lost a race to submit for a stage. Continuing mining') + this.endDoBlockChecks(); + return; + } if (this._exitOnError) { this._adapter.error(`Automatically restarting`); process.exit(1); diff --git a/test/reputation-system/reputation-mining-client/client-auto-functionality.js b/test/reputation-system/reputation-mining-client/client-auto-functionality.js index 9f79d8ea5d..b16200d698 100644 --- a/test/reputation-system/reputation-mining-client/client-auto-functionality.js +++ b/test/reputation-system/reputation-mining-client/client-auto-functionality.js @@ -20,6 +20,12 @@ const { getWaitForNSubmissionsPromise, getMiningCycleCompletePromise, TestAdapter, + getBlock, + web3GetTransactionReceipt, + web3GetTransaction, + sleep, + stopMining, + startMining, } = require("../../../helpers/test-helper"); const { setupColonyNetwork, @@ -260,6 +266,188 @@ process.env.SOLIDITY_COVERAGE await miningCycleComplete; }); + it("miners should be randomised in terms of order of allowed responses each cycle", async function () { + reputationMinerClient._processingDelay = 1; + const reputationMinerClient2 = new ReputationMinerClient({ + loader, + realProviderPort, + minerAddress: MINER2, + useJsTree: true, + auto: true, + oracle: false, + processingDelay: 1, + }); + await reputationMinerClient.initialise(colonyNetwork.address, startingBlockNumber); + await reputationMinerClient2.initialise(colonyNetwork.address, startingBlockNumber); + await mineBlock(); + + let differentAddresses = false; + const completionAddresses = []; + while (!differentAddresses) { + const repCycleEthers = await reputationMinerClient._miner.getActiveRepCycle(); + const receive12Submissions = getWaitForNSubmissionsPromise(repCycleEthers, null, null, null, 12); + + // Forward time and wait for the client to submit all 12 allowed entries + await forwardTime(MINING_CYCLE_DURATION / 2, this); + await receive12Submissions; + + let cycleComplete = false; + let error = false; + const colonyNetworkEthers = reputationMinerClient._miner.colonyNetwork; + let completionEvent; + const miningCycleCompletePromise = new Promise(function (resolve, reject) { + colonyNetworkEthers.on("ReputationMiningCycleComplete", async (_hash, _nLeaves, event) => { + event.removeListener(); + cycleComplete = true; + completionEvent = event; + resolve(); + }); + + // After 30s, we throw a timeout error + setTimeout(() => { + error = true; + reject(new Error("ERROR: timeout while waiting for confirming hash")); + }, 30000); + }); + + while (!cycleComplete && !error) { + await forwardTime(MINING_CYCLE_DURATION / 10); + await sleep(1000); + } + + if (error) { + throw miningCycleCompletePromise; + } + + const t = await completionEvent.getTransaction(); + completionAddresses.push(t.from); + // We repeat this loop until both miners have confirmed in different cycles + if ([...new Set(completionAddresses)].length > 1) { + differentAddresses = true; + } + } + await reputationMinerClient2.close(); + reputationMinerClient._processingDelay = 10; + }); + + it("Losing a race shouldn't prevent a miner from continuing", async function () { + reputationMinerClient._processingDelay = 1; + const SUBMISSION_SIG = web3.utils.soliditySha3("submitRootHash(bytes32,uint256,bytes32,uint256)").slice(0, 10); + + const reputationMinerClient2 = new ReputationMinerClient({ + loader, + realProviderPort, + minerAddress: MINER3, + useJsTree: true, + auto: true, + oracle: false, + processingDelay: 1, + }); + await reputationMinerClient2.initialise(colonyNetwork.address, startingBlockNumber); + + let lostRace = false; + while (reputationMinerClient.lockedForBlockProcessing || reputationMinerClient2.lockedForBlockProcessing) { + await sleep(1000); + } + reputationMinerClient.lockedForBlockProcessing = true; + reputationMinerClient2.lockedForBlockProcessing = true; + await mineBlock(); + + let latestBlock = await currentBlock(); + let firstSubmissionBlockNumber = latestBlock.number; + + let repCycleEthers = await reputationMinerClient._miner.getActiveRepCycle(); + let receive12Submissions = getWaitForNSubmissionsPromise(repCycleEthers, null, null, null, 12); + + await forwardTime(MINING_CYCLE_DURATION / 2, this); + const oldHash = await colonyNetwork.getReputationRootHash(); + + await goodClient.loadState(oldHash); + await goodClient.addLogContentsToReputationTree(); + for (let i = 0; i < 11; i += 1) { + await goodClient.submitRootHash(); + } + await stopMining(); + + const submissionIndex1 = reputationMinerClient.submissionIndex; + const submissionIndex2 = reputationMinerClient2.submissionIndex; + reputationMinerClient.lockedForBlockProcessing = false; + reputationMinerClient2.lockedForBlockProcessing = false; + + await mineBlock(); + while (reputationMinerClient.submissionIndex === submissionIndex1 || reputationMinerClient2.submissionIndex === submissionIndex2) { + await sleep(1000); + } + + await startMining(); + await mineBlock(); + + await receive12Submissions; + // Forward time to the end of the mining cycle and since we are the only miner, check the client confirmed our hash correctly + await forwardTime(MINING_CYCLE_DURATION / 2 + CHALLENGE_RESPONSE_WINDOW_DURATION + 1, this); + + await goodClient.confirmNewHash(); + let endBlock = await currentBlock(); + let endBlockNumber = endBlock.number; + // For every block... + for (let i = firstSubmissionBlockNumber; i <= endBlockNumber; i += 1) { + const block = await getBlock(i); + // Check every transaction... + for (let txCount = 0; txCount < block.transactions.length; txCount += 1) { + const txHash = block.transactions[txCount]; + const txReceipt = await web3GetTransactionReceipt(txHash); + if (!txReceipt.status) { + // Was it actually a race? + const tx = await web3GetTransaction(txHash); + if (tx.input.slice(0, 10) === SUBMISSION_SIG) { + lostRace = true; + } + } + } + } + // } + + assert(lostRace, "No lostrace seen"); + + // So we've now seen a miner lose a race - let's check they can go through a cycle correctly. + repCycleEthers = await reputationMinerClient._miner.getActiveRepCycle(); + receive12Submissions = getWaitForNSubmissionsPromise(repCycleEthers, null, null, null, 12); + + latestBlock = await currentBlock(); + firstSubmissionBlockNumber = latestBlock.number; + // Forward time and wait for the clients to submit all 12 allowed entries + await forwardTime(MINING_CYCLE_DURATION / 2, this); + + await receive12Submissions; + endBlock = await currentBlock(); + endBlockNumber = endBlock.number; + + const submissionAddresses = []; + + // For every block... + for (let i = firstSubmissionBlockNumber; i <= endBlockNumber; i += 1) { + const block = await getBlock(i); + // Check every transaction... + for (let txCount = 0; txCount < block.transactions.length; txCount += 1) { + const txHash = block.transactions[txCount]; + const tx = await web3GetTransaction(txHash); + if (tx.input.slice(0, 10) === SUBMISSION_SIG) { + submissionAddresses.push(tx.from); + } + } + } + + // If we are locked for block processing (for example, after stopping block checks due to an error), this will hang + // Reset everything to be well behaved before testing the assertion. + reputationMinerClient2.lockedForBlockProcessing = false; + await reputationMinerClient2.close(); + reputationMinerClient._processingDelay = 10; + + if ([...new Set(submissionAddresses)].length === 1) { + assert(false, "Only one miner address seen"); + } + }); + it("should successfully complete a dispute resolution", async function () { const badClient = new MaliciousReputationMinerExtraRep({ loader, realProviderPort, useJsTree: true, minerAddress: MINER3 }, 1, 0); await badClient.initialise(colonyNetwork.address);