Skip to content

Commit

Permalink
Merge branch 'refs/heads/main' into mike/uups
Browse files Browse the repository at this point in the history
  • Loading branch information
Mike-CZ committed Nov 10, 2024
2 parents 9afae01 + e482950 commit 81f0e76
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 0 deletions.
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 OwnableUpgradeable {
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 OwnableUpgradeable {
}
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();
}
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 @@ -38,6 +38,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 @@ -18,6 +18,7 @@ contract SFC is Initializable, OwnableUpgradeable, UUPSUpgradeable, 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 @@ -69,13 +70,25 @@ contract SFC is Initializable, OwnableUpgradeable, UUPSUpgradeable, 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 @@ -294,6 +307,7 @@ contract SFC is Initializable, OwnableUpgradeable, UUPSUpgradeable, Version {
epochDuration = _now() - prevSnapshot.endTime;
}
_sealEpochRewards(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes, originatedTxsFee);
_sealEpochAverageUptime(epochDuration, snapshot, prevSnapshot, validatorIDs, uptimes);
}

currentSealedEpoch = currentEpoch();
Expand Down Expand Up @@ -526,6 +540,11 @@ contract SFC is Initializable, OwnableUpgradeable, UUPSUpgradeable, 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 @@ -907,6 +926,64 @@ contract SFC is Initializable, OwnableUpgradeable, UUPSUpgradeable, 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 @@ -945,4 +945,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,
);
});
});
});

0 comments on commit 81f0e76

Please sign in to comment.