Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add average uptime calculation #97

Merged
merged 8 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/interfaces/ISFC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ interface ISFC {

function getEpochAccumulatedUptime(uint256 epoch, uint256 validatorID) external view returns (uint256);

function getEpochAverageUptime(uint256 epoch, uint256 validatorID) external view returns (uint32);

function getEpochAccumulatedOriginatedTxsFee(uint256 epoch, uint256 validatorID) external view returns (uint256);

function getEpochOfflineTime(uint256 epoch, uint256 validatorID) external view returns (uint256);
Expand Down
26 changes: 26 additions & 0 deletions contracts/sfc/ConstantsManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ contract ConstantsManager is Ownable {
uint256 public targetGasPowerPerSecond;
uint256 public gasPriceBalancingCounterweight;

// The number of epochs to calculate the average uptime ratio from, acceptable bound [10, 87600].
// Is also the minimum number of epochs necessary for deactivation of offline validators.
uint32 public averageUptimeEpochWindow;

// Minimum average uptime ratio in fixed-point format; acceptable bounds [0,0.9].
// Zero to disable validators deactivation by this metric.
uint64 public minAverageUptime;

/**
* @dev Given value is too small
*/
Expand Down Expand Up @@ -153,4 +161,22 @@ contract ConstantsManager is Ownable {
}
gasPriceBalancingCounterweight = v;
}

function updateAverageUptimeEpochWindow(uint32 v) external virtual onlyOwner {
if (v < 10) {
// needs to be long enough to allow permissible downtime for validators maintenance
revert ValueTooSmall();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the comment, you may want to mention that an epoch lasts approximately 6 minutes, and the duration 6-minute * threshold must exceed the permissible downtime with a safety margin so that validators going through a maintenance cycle are not automatically culled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should fix and mention the epoch duration in SFC source, as it depends on the network configuration. We may use very different epochs configuration in the future.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.

}
if (v > 87600) {
revert ValueTooLarge();
}
averageUptimeEpochWindow = v;
}

function updateMinAverageUptime(uint64 v) external virtual onlyOwner {
if (v > ((Decimal.unit() * 9) / 10)) {
revert ValueTooLarge();
}
minAverageUptime = v;
}
}
2 changes: 2 additions & 0 deletions contracts/sfc/NetworkInitializer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ contract NetworkInitializer {
consts.updateOfflinePenaltyThresholdBlocksNum(1000);
consts.updateTargetGasPowerPerSecond(2000000);
consts.updateGasPriceBalancingCounterweight(3600);
consts.updateAverageUptimeEpochWindow(100);
consts.updateMinAverageUptime(0); // check disabled by default
consts.transferOwnership(_owner);

ISFC(_sfc).initialize(sealedEpoch, totalSupply, _auth, address(consts), _owner);
Expand Down
77 changes: 77 additions & 0 deletions contracts/sfc/SFC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ contract SFC is Initializable, Ownable, Version {
uint256 internal constant OK_STATUS = 0;
uint256 internal constant WITHDRAWN_BIT = 1;
uint256 internal constant OFFLINE_BIT = 1 << 3;
uint256 internal constant OFFLINE_AVG_BIT = 1 << 4;
uint256 internal constant DOUBLESIGN_BIT = 1 << 7;
uint256 internal constant CHEATER_MASK = DOUBLESIGN_BIT;

Expand Down Expand Up @@ -68,13 +69,25 @@ contract SFC is Initializable, Ownable, Version {
// delegator => validator ID => current stake
mapping(address delegator => mapping(uint256 validatorID => uint256 stake)) public getStake;

// data structure to compute average uptime for each active validator
struct AverageUptime {
// average uptime ratio as a value between 0 and 1e18
uint64 averageUptime;
// remainder from the division in the average calculation
uint32 remainder;
// number of epochs in the average (at most averageUptimeEpochsWindow)
uint32 epochs;
}

struct EpochSnapshot {
// validator ID => validator weight in the epoch
mapping(uint256 => uint256) receivedStake;
// validator ID => accumulated ( delegatorsReward * 1e18 / receivedStake )
mapping(uint256 => uint256) accumulatedRewardPerToken;
// validator ID => accumulated online time
mapping(uint256 => uint256) accumulatedUptime;
// validator ID => average uptime as a percentage
mapping(uint256 => AverageUptime) averageUptime;
// validator ID => gas fees from txs originated by the validator
mapping(uint256 => uint256) accumulatedOriginatedTxsFee;
mapping(uint256 => uint256) offlineTime;
Expand Down Expand Up @@ -288,6 +301,7 @@ contract SFC is Initializable, Ownable, Version {
epochDuration = _now() - prevSnapshot.endTime;
}
_sealEpochRewards(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes, originatedTxsFee);
_sealEpochAverageUptime(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes);
}

currentSealedEpoch = currentEpoch();
Expand Down Expand Up @@ -520,6 +534,11 @@ contract SFC is Initializable, Ownable, Version {
return getEpochSnapshot[epoch].accumulatedUptime[validatorID];
}

/// Get average uptime for a validator in a given epoch.
function getEpochAverageUptime(uint256 epoch, uint256 validatorID) public view returns (uint64) {
return getEpochSnapshot[epoch].averageUptime[validatorID].averageUptime;
}

/// Get accumulated originated txs fee for a validator in a given epoch.
function getEpochAccumulatedOriginatedTxsFee(uint256 epoch, uint256 validatorID) public view returns (uint256) {
return getEpochSnapshot[epoch].accumulatedOriginatedTxsFee[validatorID];
Expand Down Expand Up @@ -901,6 +920,64 @@ contract SFC is Initializable, Ownable, Version {
}
}

/// Seal epoch - recalculate average uptime time of validators
function _sealEpochAverageUptime(
uint256 epochDuration,
EpochSnapshot storage snapshot,
EpochSnapshot storage prevSnapshot,
uint256[] memory validatorIDs,
uint256[] memory uptimes
) internal {
for (uint256 i = 0; i < validatorIDs.length; i++) {
uint256 validatorID = validatorIDs[i];
// compute normalised uptime as a percentage in the fixed-point format
uint256 normalisedUptime = (uptimes[i] * Decimal.unit()) / epochDuration;
if (normalisedUptime > Decimal.unit()) {
normalisedUptime = Decimal.unit();
}
AverageUptime memory previous = prevSnapshot.averageUptime[validatorID];
AverageUptime memory current = _addElementIntoAverageUptime(uint64(normalisedUptime), previous);
snapshot.averageUptime[validatorID] = current;

// remove validator if average uptime drops below min average uptime
// (by setting minAverageUptime to zero, this check is ignored)
if (current.averageUptime < c.minAverageUptime() && current.epochs >= c.averageUptimeEpochWindow()) {
_setValidatorDeactivated(validatorID, OFFLINE_AVG_BIT);
_syncValidator(validatorID, false);
}
}
}

function _addElementIntoAverageUptime(
uint64 newValue,
AverageUptime memory prev
) private view returns (AverageUptime memory) {
AverageUptime memory cur;
if (prev.epochs == 0) {
cur.averageUptime = newValue; // the only element for the average
cur.epochs = 1;
return cur;
}

// the number of elements the average is calculated from
uint128 n = prev.epochs + 1;
// add new value into the average
uint128 tmp = (n - 1) * uint128(prev.averageUptime) + uint128(newValue) + prev.remainder;

cur.averageUptime = uint64(tmp / n);
cur.remainder = uint32(tmp % n);

if (cur.averageUptime > Decimal.unit()) {
cur.averageUptime = uint64(Decimal.unit());
}
if (prev.epochs < c.averageUptimeEpochWindow()) {
cur.epochs = prev.epochs + 1;
} else {
cur.epochs = prev.epochs;
}
return cur;
}

/// Create a new validator.
function _createValidator(address auth, bytes calldata pubkey) internal {
uint256 validatorID = ++lastValidatorID;
Expand Down
2 changes: 2 additions & 0 deletions contracts/test/UnitTestSFC.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ contract UnitTestNetworkInitializer {
consts.updateOfflinePenaltyThresholdBlocksNum(1000);
consts.updateTargetGasPowerPerSecond(2000000);
consts.updateGasPriceBalancingCounterweight(6 * 60 * 60);
consts.updateAverageUptimeEpochWindow(10);
consts.updateMinAverageUptime(0); // check disabled by default
consts.transferOwnership(_owner);

ISFC(_sfc).initialize(sealedEpoch, totalSupply, _auth, address(consts), _owner);
Expand Down
78 changes: 78 additions & 0 deletions test/SFC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,4 +951,82 @@ describe('SFC', () => {
await expect(this.sfc._syncValidator(33, false)).to.be.revertedWithCustomError(this.sfc, 'ValidatorNotExists');
});
});

describe('Average uptime calculation', () => {
const validatorsFixture = async function (this: Context) {
const [validator] = await ethers.getSigners();
const pubkey =
'0xc000a2941866e485442aa6b17d67d77f8a6c4580bb556894cc1618473eff1e18203d8cce50b563cf4c75e408886079b8f067069442ed52e2ac9e556baa3f8fcc525f';
const blockchainNode = new BlockchainNode(this.sfc);

await this.sfc.rebaseTime();
await this.sfc.enableNonNodeCalls();

await blockchainNode.handleTx(
await this.sfc.connect(validator).createValidator(pubkey, { value: ethers.parseEther('10') }),
);

const validatorId = await this.sfc.getValidatorID(validator);

await blockchainNode.sealEpoch(0);

return {
validatorId,
blockchainNode,
};
};

beforeEach(async function () {
return Object.assign(this, await loadFixture(validatorsFixture.bind(this)));
});

it('Should calculate uptime correctly', async function () {
// validator online 100% of time in the first epoch => average 100%
await this.blockchainNode.sealEpoch(
100,
new Map<number, ValidatorMetrics>([[this.validatorId as number, new ValidatorMetrics(0, 0, 100, 0n)]]),
);
expect(await this.sfc.getEpochAverageUptime(await this.sfc.currentSealedEpoch(), this.validatorId)).to.equal(
1000000000000000000n,
);

// validator online 20% of time in the second epoch => average 60%
await this.blockchainNode.sealEpoch(
100,
new Map<number, ValidatorMetrics>([[this.validatorId as number, new ValidatorMetrics(0, 0, 20, 0n)]]),
);
expect(await this.sfc.getEpochAverageUptime(await this.sfc.currentSealedEpoch(), this.validatorId)).to.equal(
600000000000000000n,
);

// validator online 30% of time in the third epoch => average 50%
await this.blockchainNode.sealEpoch(
100,
new Map<number, ValidatorMetrics>([[this.validatorId as number, new ValidatorMetrics(0, 0, 30, 0n)]]),
);
expect(await this.sfc.getEpochAverageUptime(await this.sfc.currentSealedEpoch(), this.validatorId)).to.equal(
500000000000000000n,
);

// fill the averaging window
for (let i = 0; i < 10; i++) {
await this.blockchainNode.sealEpoch(
100,
new Map<number, ValidatorMetrics>([[this.validatorId as number, new ValidatorMetrics(0, 0, 50, 0n)]]),
);
expect(await this.sfc.getEpochAverageUptime(await this.sfc.currentSealedEpoch(), this.validatorId)).to.equal(
500000000000000000n,
);
}

// (50 * 10 + 28) / 11 = 48
await this.blockchainNode.sealEpoch(
100,
new Map<number, ValidatorMetrics>([[this.validatorId as number, new ValidatorMetrics(0, 0, 28, 0n)]]),
);
expect(await this.sfc.getEpochAverageUptime(await this.sfc.currentSealedEpoch(), this.validatorId)).to.equal(
480000000000000000n,
);
});
});
});