Skip to content

Commit

Permalink
Merge pull request #6 from Kotsin/i-90-validator-set-upscaling
Browse files Browse the repository at this point in the history
I 90 validator set upscaling
  • Loading branch information
Kotsin authored Dec 22, 2022
2 parents 1d97a53 + f92d28e commit 86f4214
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 23 deletions.
48 changes: 28 additions & 20 deletions contracts/base/BlockRewardHbbftBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -255,38 +255,45 @@ 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 (
currentTimestamp >= stakingContract.stakingFixedEpochEndTime()
) {
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();
// }
// }
// }
}
}

Expand Down Expand Up @@ -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()
Expand All @@ -509,7 +517,7 @@ contract BlockRewardHbbftBase is UpgradeableOwned, IBlockRewardHbbft {
return
validatorSetContract.getCurrentTimestamp() >
stakingContract.stakingFixedEpochEndTime()
? 1000
? 100
: ((validatorSetContract.getCurrentTimestamp() -
stakingContract.stakingEpochStartTime()) * 100) /
expectedEpochDuration;
Expand Down
5 changes: 5 additions & 0 deletions contracts/interfaces/IValidatorSetHbbft.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const config: {} = {
networks: {
hardhat: {
accounts: {
count: 60,
count: 100,
mnemonic,
accountsBalance: "1000000000000000000000000000"
},
Expand Down
132 changes: 130 additions & 2 deletions test/BlockRewardHbbft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ValidatorSetHbbftMock,
StakingHbbftCoinsMock,
KeyGenHistory,
IStakingHbbft
IStakingHbbft,
} from "../src/types";

import fp from 'lodash/fp';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<T>(arr1: T[], arr2: T[]): void {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -510,14 +574,78 @@ 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);
}

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);
}
}

0 comments on commit 86f4214

Please sign in to comment.