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 4 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;

// Epoch threshold for stop counting alive epochs (avoid diminishing impact of new uptimes).
// Is also the minimum number of epochs necessary for deactivation of offline validators.
uint32 public averageUptimeEpochsThreshold;

// Minimum average uptime in Q1.30 format; acceptable bounds [0,0.9]
Copy link

Choose a reason for hiding this comment

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

Changing from int32 to uint32 means we have UQ1.31 (and not Q1.30). Perhaps, you want to replace any occurrence in the comments/source code from Q1.30 to UQ1.31. Also, the constant 1 << 30 needs to change to 1 << 31. Ideally, you don't want to have the number 1 << 31 in the source code - perhaps you can define a constant with the value 1 << 31.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actually we have already Decimal.unit() in the SFC for this purpose.
Reworked to use Decimal.unit() instead.

// Zero to disable validators deactivation by this metric.
uint32 public minAverageUptime;

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

function updateAverageUptimeEpochsThreshold(uint32 v) external virtual onlyOwner {
if (v < 10) {
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();
}
averageUptimeEpochsThreshold = v;
}

function updateMinAverageUptime(uint32 v) external virtual onlyOwner {
if (v > 966367641) {
// 0.9 in Q1.30
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.updateAverageUptimeEpochsThreshold(100);
consts.updateMinAverageUptime(0); // check disabled by default
consts.transferOwnership(_owner);

ISFC(_sfc).initialize(sealedEpoch, totalSupply, _auth, address(consts), _owner);
Expand Down
83 changes: 83 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 (1 << 30)
uint32 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 (uint32) {
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,70 @@ 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 Q1.30 fixed-point format
Copy link

Choose a reason for hiding this comment

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

Q1.30 => UQ1.31

uint256 normalisedUptime = (uptimes[i] * (1 << 30)) / epochDuration;
Copy link

Choose a reason for hiding this comment

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

1 << 30 => 1 << 31 (Perhaps use a constant somewhere for the fixpoint such as const UQ1.0 = 1 << 31; and use the symbol instead of 1<<31.

if (normalisedUptime > 1 << 30) {
Copy link

Choose a reason for hiding this comment

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

1 << 30 => 1 << 31

normalisedUptime = 1 << 30;
}
AverageUptime memory previous = prevSnapshot.averageUptime[validatorID];
AverageUptime memory current = _addElementIntoAverageUptime(uint32(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.averageUptimeEpochsThreshold()) {
_setValidatorDeactivated(validatorID, OFFLINE_AVG_BIT);
_syncValidator(validatorID, false);
}
}
}

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

// the number of elements to calculate the average from
uint64 n = prev.epochs + 1;
// use lemma to add new value into the average
uint64 tmp = (n - 1) * uint64(prev.averageUptime) + uint64(newValue);

// apply remainder from the division in the previous iteration to reduce error
if (prev.remainder != 0) {
tmp += (n * prev.remainder) / (n - 1);
}

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

if (cur.averageUptime > 1 << 30) {
cur.averageUptime = 1 << 30;
}
if (prev.epochs < c.averageUptimeEpochsThreshold()) {
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