diff --git a/contracts/base/BlockRewardHbbftBase.sol b/contracts/base/BlockRewardHbbftBase.sol index fe0a2416..fd6e1113 100644 --- a/contracts/base/BlockRewardHbbftBase.sol +++ b/contracts/base/BlockRewardHbbftBase.sol @@ -255,15 +255,38 @@ contract BlockRewardHbbftBase is UpgradeableOwned, IBlockRewardHbbft { uint256 currentTimestamp = validatorSetContract .getCurrentTimestamp(); + address[] memory miningAddresses = validatorSetContract + .getValidators(); + // TODO: Problem occurs here if there are not regular blocks: // https://github.com/DMDcoin/hbbft-posdao-contracts/issues/96 //we are in a transition to phase 2 if the time for it arrived, // and we do not have pendingValidators yet. - bool isPhaseTransition = currentTimestamp >= phaseTransitionTime && - validatorSetContract.getPendingValidators().length == 0; + bool isPhaseTransition = currentTimestamp >= phaseTransitionTime; + bool toBeUpscaled; + if ( + miningAddresses.length <= + (validatorSetContract.maxValidators() / 3) * 2 + ) { + uint256 amountToBeElected = stakingContract + .getPoolsToBeElected() + .length; + if ( + (amountToBeElected > 0) && + validatorSetContract.getValidatorCountSweetSpot( + amountToBeElected + ) > + miningAddresses.length + ) { + toBeUpscaled = true; + } + } - if (isPhaseTransition) { + if ( + (isPhaseTransition || toBeUpscaled) && + validatorSetContract.getPendingValidators().length == 0 + ) { // Choose new validators validatorSetContract.newValidatorSet(); } else if ( @@ -271,22 +294,6 @@ contract BlockRewardHbbftBase is UpgradeableOwned, IBlockRewardHbbft { ) { validatorSetContract.handleFailedKeyGeneration(); } - // } else { - - // // check for faster validator set upscaling - // // https://github.com/DMDcoin/hbbft-posdao-contracts/issues/90 - - // address[] memory miningAddresses = validatorSetContract.getValidators(); - - // // if there is a miningset that is smaller than the 2/3 of the maxValidators, - // // then we choose the next epoch set. - // if (miningAddresses.length < (validatorSetContract.maxValidators() / 3) * 2) { - // address[] memory poolsToBeElected = stakingContract.getPoolsToBeElected(); - // if (poolsToBeElected.length > miningAddresses.length) { - // validatorSetContract.newValidatorSet(); - // } - // } - // } } } @@ -499,6 +506,7 @@ contract BlockRewardHbbftBase is UpgradeableOwned, IBlockRewardHbbft { } ///@dev Calculates and returns the percentage of the current epoch. + /// 100% MAX function epochPercentage() public view returns (uint256) { IStakingHbbft stakingContract = IStakingHbbft( validatorSetContract.getStakingContract() @@ -509,7 +517,7 @@ contract BlockRewardHbbftBase is UpgradeableOwned, IBlockRewardHbbft { return validatorSetContract.getCurrentTimestamp() > stakingContract.stakingFixedEpochEndTime() - ? 1000 + ? 100 : ((validatorSetContract.getCurrentTimestamp() - stakingContract.stakingEpochStartTime()) * 100) / expectedEpochDuration; diff --git a/contracts/interfaces/IValidatorSetHbbft.sol b/contracts/interfaces/IValidatorSetHbbft.sol index c7998b5a..e6cd941b 100644 --- a/contracts/interfaces/IValidatorSetHbbft.sol +++ b/contracts/interfaces/IValidatorSetHbbft.sol @@ -90,4 +90,9 @@ interface IValidatorSetHbbft { function getCurrentTimestamp() external view returns (uint256); function validatorAvailableSince(address) external view returns (uint256); + + function getValidatorCountSweetSpot(uint256) + external + view + returns (uint256); } diff --git a/hardhat.config.ts b/hardhat.config.ts index 66f05fc5..e8eb91eb 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -95,7 +95,7 @@ const config: {} = { networks: { hardhat: { accounts: { - count: 60, + count: 100, mnemonic, accountsBalance: "1000000000000000000000000000" }, diff --git a/test/BlockRewardHbbft.ts b/test/BlockRewardHbbft.ts index 8b5b13c9..62917c7f 100644 --- a/test/BlockRewardHbbft.ts +++ b/test/BlockRewardHbbft.ts @@ -7,7 +7,7 @@ import { ValidatorSetHbbftMock, StakingHbbftCoinsMock, KeyGenHistory, - IStakingHbbft + IStakingHbbft, } from "../src/types"; import fp from 'lodash/fp'; @@ -131,6 +131,7 @@ describe('BlockRewardHbbft', () => { // The IP addresses are irrelevant for these unit test, just initialize them to 0. initialValidatorsIpAddresses = ['0x00000000000000000000000000000000', '0x00000000000000000000000000000000', '0x00000000000000000000000000000000']; + await validatorSetHbbft.setCurrentTimestamp((await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp) // Initialize ValidatorSetHbbft await validatorSetHbbft.initialize( @@ -181,6 +182,7 @@ describe('BlockRewardHbbft', () => { [[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0, 0, 0, 4, 239, 1, 112, 13, 13, 251, 103, 186, 212, 78, 44, 47, 250, 221, 84, 118, 88, 7, 64, 206, 186, 11, 2, 8, 204, 140, 106, 179, 52, 251, 237, 19, 53, 74, 187, 217, 134, 94, 66, 68, 89, 42, 85, 207, 155, 220, 101, 223, 51, 199, 37, 38, 203, 132, 13, 77, 78, 114, 53, 219, 114, 93, 21, 25, 164, 12, 43, 252, 160, 16, 23, 111, 79, 230, 121, 95, 223, 174, 211, 172, 231, 0, 52, 25, 49, 152, 79, 128, 39, 117, 216, 85, 201, 237, 242, 151, 219, 149, 214, 77, 233, 145, 47, 10, 184, 175, 162, 174, 237, 177, 131, 45, 126, 231, 32, 147, 227, 170, 125, 133, 36, 123, 164, 232, 129, 135, 196, 136, 186, 45, 73, 226, 179, 169, 147, 42, 41, 140, 202, 191, 12, 73, 146, 2]], [[0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 145, 0, 0, 0, 0, 0, 0, 0, 4, 239, 1, 112, 13, 13, 251, 103, 186, 212, 78, 44, 47, 250, 221, 84, 118, 88, 7, 64, 206, 186, 11, 2, 8, 204, 140, 106, 179, 52, 251, 237, 19, 53, 74, 187, 217, 134, 94, 66, 68, 89, 42, 85, 207, 155, 220, 101, 223, 51, 199, 37, 38, 203, 132, 13, 77, 78, 114, 53, 219, 114, 93, 21, 25, 164, 12, 43, 252, 160, 16, 23, 111, 79, 230, 121, 95, 223, 174, 211, 172, 231, 0, 52, 25, 49, 152, 79, 128, 39, 117, 216, 85, 201, 237, 242, 151, 219, 149, 214, 77, 233, 145, 47, 10, 184, 175, 162, 174, 237, 177, 131, 45, 126, 231, 32, 147, 227, 170, 125, 133, 36, 123, 164, 232, 129, 135, 196, 136, 186, 45, 73, 226, 179, 169, 147, 42, 41, 140, 202, 191, 12, 73, 146, 2]]] ) + }); it('staking epoch #0 finished', async () => { @@ -454,6 +456,63 @@ describe('BlockRewardHbbft', () => { actualValidatorReward.should.be.closeTo(expectedValidatorReward, expectedValidatorReward.div(100000)); }) + + describe("Upscaling tests", async () => { + it("Add multiple validator pools and upscale if needed.", async () => { + const accountAddresses = accounts.map(item => item.address); + const additionalValidators = accountAddresses.slice(7, 52 + 1); // accounts[7...32] + const additionalStakingAddresses = accountAddresses.slice(53, 99 + 1); // accounts[33...59] + + additionalValidators.length.should.be.equal(46); + additionalStakingAddresses.length.should.be.equal(46); + + await network.provider.send("evm_setIntervalMining", [8]); + + for (let i = 0; i < additionalValidators.length; i++) { + let stakingAddress = await ethers.getSigner(additionalStakingAddresses[i]); + let miningAddress = await ethers.getSigner(additionalValidators[i]); + + await stakingHbbft.connect(stakingAddress).addPool(miningAddress.address, '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + '0x00000000000000000000000000000000', { value: MIN_STAKE }); + await announceAvailability(miningAddress.address); + await mine(); + + let toBeElected = (await stakingHbbft.getPoolsToBeElected()).length; + let pendingValidators = (await validatorSetHbbft.getPendingValidators()).length + if (toBeElected > 4 && toBeElected <= 19 && pendingValidators == 0) { + (await validatorSetHbbft.getValidatorCountSweetSpot((await stakingHbbft.getPoolsToBeElected()).length)).should.be.equal((await validatorSetHbbft.getValidators()).length); + } + } + + await timeTravelToTransition(); + await timeTravelToEndEpoch(); + // after epoch was finalized successfully, validator set length is healthy + (await validatorSetHbbft.getValidators()).length.should.be.eq(25); + (await stakingHbbft.getPoolsToBeElected()).length.should.be.eq(49); + }) + + it("banning validator up to 16", async () => { + await validatorSetHbbft.setSystemAddress(owner.address); + while ((await validatorSetHbbft.getValidators()).length > 16) { + await mine(); + await validatorSetHbbft.connect(owner).removeMaliciousValidators([(await validatorSetHbbft.getValidators())[13]]); + } + (await validatorSetHbbft.getValidators()).length.should.be.eq(16); + }) + it("mining twice shouldn't change pending validator set", async () => { + await callReward(false); + (await validatorSetHbbft.getPendingValidators()).length.should.be.eq(25); + let pendingValidators = await validatorSetHbbft.getPendingValidators(); + await callReward(false); + sortedEqual(pendingValidators, await validatorSetHbbft.getPendingValidators()); + }) + it("set is scaled to 25", async () => { + await mine(); + (await validatorSetHbbft.getValidators()).length.should.be.eq(25); + (await validatorSetHbbft.getPendingValidators()).length.should.be.eq(0); + await network.provider.send("evm_setIntervalMining", [0]); + }) + }) }); function sortedEqual(arr1: T[], arr2: T[]): void { @@ -490,6 +549,8 @@ async function increaseTime(time: number) { const currentTimestamp = await validatorSetHbbft.getCurrentTimestamp(); const futureTimestamp = currentTimestamp.add(BigNumber.from(time)); + await network.provider.send("evm_setNextBlockTimestamp", [futureTimestamp.toNumber()]); + await network.provider.send("evm_mine"); await validatorSetHbbft.setCurrentTimestamp(futureTimestamp); const currentTimestampAfter = await validatorSetHbbft.getCurrentTimestamp(); futureTimestamp.should.be.equal(currentTimestampAfter); @@ -501,6 +562,9 @@ async function increaseTime(time: number) { async function timeTravelToTransition() { let startTimeOfNextPhaseTransition = await stakingHbbft.startTimeOfNextPhaseTransition(); + await network.provider.send("evm_setNextBlockTimestamp", [startTimeOfNextPhaseTransition.toNumber()]); + await network.provider.send("evm_mine"); + await validatorSetHbbft.setCurrentTimestamp(startTimeOfNextPhaseTransition); const currentTS = await validatorSetHbbft.getCurrentTimestamp(); currentTS.should.be.equal(startTimeOfNextPhaseTransition); @@ -510,6 +574,8 @@ async function timeTravelToTransition() { async function timeTravelToEndEpoch() { const endTimeOfCurrentEpoch = await stakingHbbft.stakingFixedEpochEndTime(); + await network.provider.send("evm_setNextBlockTimestamp", [endTimeOfCurrentEpoch.toNumber()]); + await network.provider.send("evm_mine"); await validatorSetHbbft.setCurrentTimestamp(endTimeOfCurrentEpoch); await callReward(true); } @@ -517,7 +583,69 @@ async function timeTravelToEndEpoch() { async function finishEpochPrelim(_percentage: BigNumber) { const epochDuration = (await stakingHbbft.stakingFixedEpochEndTime()).sub((await stakingHbbft.stakingEpochStartTime())).mul(_percentage).div(100).add(1); const endTimeOfCurrentEpoch = (await stakingHbbft.stakingEpochStartTime()).add(epochDuration); + await network.provider.send("evm_setNextBlockTimestamp", [endTimeOfCurrentEpoch.toNumber()]); + await network.provider.send("evm_mine"); await validatorSetHbbft.setCurrentTimestamp(endTimeOfCurrentEpoch); - _percentage = await blockRewardHbbft.epochPercentage(); + // _percentage = await blockRewardHbbft.epochPercentage(); await callReward(true); } + +async function announceAvailability(pool: string) { + const blockNumber = await ethers.provider.getBlockNumber() + const block = await ethers.provider.getBlock(blockNumber); + const asEncoded = validatorSetHbbft.interface.encodeFunctionData("announceAvailability", [blockNumber, block.hash]); + + // we know now, that this call is allowed. + // so we can execute it. + await (await ethers.getSigner(pool)).sendTransaction({ to: validatorSetHbbft.address, data: asEncoded }); +} + +async function mine() { + let expectedEpochDuration = (await stakingHbbft + .stakingFixedEpochEndTime()).sub(await stakingHbbft.stakingEpochStartTime()); + let blocktime = expectedEpochDuration.mul(5).div(100).add(1); //5% of the epoch + // let blocksPerEpoch = 60 * 60 * 12 / blocktime; + await network.provider.send("evm_increaseTime", [blocktime.toNumber()]); + await validatorSetHbbft.setCurrentTimestamp((await validatorSetHbbft.getCurrentTimestamp()).add(blocktime)); + if ((await validatorSetHbbft.getPendingValidators()).length > 0) { + const currentValidators = await validatorSetHbbft.getValidators(); + const maxValidators = await validatorSetHbbft.maxValidators(); + stakingEpoch = await stakingHbbft.stakingEpoch(); + + const initialGovernancePotBalance = await getCurrentGovernancePotValue(); + let deltaPotValue = await blockRewardHbbft.deltaPot(); + let reinsertPotValue = await blockRewardHbbft.reinsertPot(); + let _epochPercentage = await blockRewardHbbft.epochPercentage(); + + await callReward(true); + + const currentGovernancePotBalance = await getCurrentGovernancePotValue(); + const governancePotIncrease = currentGovernancePotBalance.sub(initialGovernancePotBalance); + + + const deltaPotShare = deltaPotValue.mul(BigNumber.from(currentValidators.length)).mul(_epochPercentage).div(BigNumber.from('6000')).div(maxValidators).div(100); + const reinsertPotShare = reinsertPotValue.mul(BigNumber.from(currentValidators.length)).mul(_epochPercentage).div(BigNumber.from('6000')).div(maxValidators).div(100); + const nativeRewardUndistributed = await blockRewardHbbft.nativeRewardUndistributed(); + + const totalReward = deltaPotShare.add(reinsertPotShare).add(nativeRewardUndistributed); + const expectedDAOShare = totalReward.div(BigNumber.from('10')); + + // we expect 1 wei difference, since the reward combination from 2 pots results in that. + //expectedDAOShare.sub(governancePotIncrease).should.to.be.bignumber.lte(BigNumber.from('1')); + governancePotIncrease.should.to.be.closeTo(expectedDAOShare, expectedDAOShare.div(10000)); + + //since there are a lot of delegators, we need to calc it on a basis that pays out the validator min reward. + let minValidatorSharePercent = 100; + ///first 4 validators have delegators so they receive less DMD + if (currentValidators.length < 4) { + minValidatorSharePercent = 30; + } + + const expectedValidatorReward = totalReward.sub(expectedDAOShare).div(BigNumber.from(currentValidators.length)).mul(minValidatorSharePercent).div(BigNumber.from('100')); + const actualValidatorReward = await blockRewardHbbft.getValidatorReward(stakingEpoch, currentValidators[currentValidators.length - 1]); + + actualValidatorReward.should.be.closeTo(expectedValidatorReward, expectedValidatorReward.div(10000)); + } else { + await callReward(false); + } +}