Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a377f38
Add comment
sebastijankuzner Jun 4, 2025
ad4d5a4
Rename to validatorsCount
sebastijankuzner Jun 4, 2025
32ccef5
Remove _validatorsCount
sebastijankuzner Jun 4, 2025
4d2baec
Add _activeValidators list
sebastijankuzner Jun 4, 2025
c4988eb
Use _activeValidators for measurement
sebastijankuzner Jun 4, 2025
00e81e7
Add activeValidatorsCount
sebastijankuzner Jun 4, 2025
9558ef9
Shuffle without parameters
sebastijankuzner Jun 4, 2025
57bffb5
Use _activeValidators in calculateRoundValidators
sebastijankuzner Jun 4, 2025
da5f1b2
Add fee
sebastijankuzner Jun 4, 2025
352fba1
Collect and return fee
sebastijankuzner Jun 4, 2025
d5efd1a
Fix add tests
sebastijankuzner Jun 5, 2025
ccca8ed
Test registration
sebastijankuzner Jun 5, 2025
e8ec95f
Check balance
sebastijankuzner Jun 5, 2025
4344a4c
Check resignation
sebastijankuzner Jun 5, 2025
c0c81a1
Test fee
sebastijankuzner Jun 5, 2025
71e4014
Check refund
sebastijankuzner Jun 5, 2025
720b63f
Add tests
sebastijankuzner Jun 5, 2025
c7232b7
Improve _isActiveValidator
sebastijankuzner Jun 6, 2025
30a3e6f
Fix reentrancy
sebastijankuzner Jun 6, 2025
5c0d531
Fee optimisation
sebastijankuzner Jun 6, 2025
18fe975
Fee in initializer
sebastijankuzner Jun 6, 2025
4acab12
Test fee
sebastijankuzner Jun 6, 2025
982a32d
Merge branch 'develop' into feat/contracts/validator-fee
sebastijankuzner Jun 9, 2025
322e9be
Improve packing
sebastijankuzner Jun 9, 2025
1677775
Set fee to uint128
sebastijankuzner Jun 9, 2025
c55034f
Update contract
sebastijankuzner Jun 9, 2025
94c9a6a
Fix deployer
sebastijankuzner Jun 9, 2025
e429496
Set milestone
sebastijankuzner Jun 10, 2025
1b30560
Support validatorRegistrationFee option
sebastijankuzner Jun 10, 2025
83e441c
Calculate total supply
sebastijankuzner Jun 11, 2025
7f4ee0b
Set validatorRegistrationFee
sebastijankuzner Jun 11, 2025
7d16611
Generate network
sebastijankuzner Jun 11, 2025
6575755
style: resolve style guide violations
sebastijankuzner Jun 11, 2025
dd93074
regenerate networks
oXtxNt9U Jun 11, 2025
a3f03c4
fixes
oXtxNt9U Jun 12, 2025
ce76a99
style: resolve style guide violations
oXtxNt9U Jun 12, 2025
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
162 changes: 136 additions & 26 deletions contracts/src/consensus/ConsensusV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Own

contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
struct ValidatorData {
uint256 votersCount;
uint256 voteBalance;
uint128 fee;
uint64 votersCount;
bool isResigned;
bytes blsPublicKey; // 96 bits
}
Expand Down Expand Up @@ -63,16 +64,15 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
address validator;
}

event FeeUpdated(uint256 fee);
event ValidatorRegistered(address addr, bytes blsPublicKey);

event ValidatorUpdated(address addr, bytes blsPublicKey);

event ValidatorResigned(address addr);

event Voted(address voter, address validator);

event Unvoted(address voter, address validator);

error InvalidFee();
error RefundFailed();
error CallerIsNotValidator();
error ValidatorNotRegistered();
error ValidatorAlreadyRegistered();
Expand All @@ -96,8 +96,9 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
mapping(address => ValidatorData) private _validatorsData;
mapping(address => bool) private _hasValidator;
mapping(bytes32 => bool) private _blsPublicKeys;
address[] private _validators;
uint256 private _validatorsCount; // Default 0
address[] private _validators; // All registered validators including resigned
address[] private _activeValidators; // Has valid BLS public key and is not resigned
mapping(address => uint256) private _activeValidatorIndex; // Points to index in _activeValidators array
uint256 private _resignedValidatorsCount; // Default 0

mapping(address => Vote) private _voters;
Expand All @@ -110,19 +111,26 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
address private _roundValidatorsHead; // Default address(0)
uint256 private _roundValidatorsCount; // Default 0
uint256 private _minValidators; // Default 1
uint128 private _fee; // Validator registration fee: Default 0

RoundValidator[][] private _rounds;

// Initializers
function initialize() public initializer {
function initialize(uint128 registrationFee) public initializer {
__Ownable_init(msg.sender);
_minValidators = 1;
_fee = registrationFee;
}

// Overrides
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}

// External functions
function setFee(uint128 registrationFee) external onlyOwner {
_fee = registrationFee;
emit FeeUpdated(registrationFee);
}

function addValidator(address addr, bytes calldata blsPublicKey, bool isResigned) external onlyOwner {
if (_rounds.length > 0) {
revert ImportIsNotAllowed();
Expand All @@ -142,9 +150,8 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
}

ValidatorData memory validator =
ValidatorData({votersCount: 0, voteBalance: 0, isResigned: isResigned, blsPublicKey: blsPublicKey});
ValidatorData({votersCount: 0, voteBalance: 0, fee: 0, isResigned: isResigned, blsPublicKey: blsPublicKey});

_validatorsCount++;
_hasValidator[addr] = true;
_validatorsData[addr] = validator;
_validators.push(addr);
Expand All @@ -153,6 +160,10 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
_resignedValidatorsCount++;
}

if (_canBecomeActiveValidator(addr)) {
_addActiveValidator(addr);
}

emit ValidatorRegistered(addr, blsPublicKey);
}

Expand Down Expand Up @@ -197,20 +208,29 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
emit Voted(voter, validator);
}

function registerValidator(bytes calldata blsPublicKey) external {
function registerValidator(bytes calldata blsPublicKey) external payable {
if (msg.value != _fee) {
revert InvalidFee();
}

if (_hasValidator[msg.sender]) {
revert ValidatorAlreadyRegistered();
}

_verifyAndRegisterBlsPublicKey(blsPublicKey);

ValidatorData memory validator =
ValidatorData({votersCount: 0, voteBalance: 0, isResigned: false, blsPublicKey: blsPublicKey});
ValidatorData memory validator = ValidatorData({
votersCount: 0,
voteBalance: 0,
fee: uint128(msg.value),
isResigned: false,
blsPublicKey: blsPublicKey
});

_validatorsCount++;
_hasValidator[msg.sender] = true;
_validatorsData[msg.sender] = validator;
_validators.push(msg.sender);
_addActiveValidator(msg.sender);

emit ValidatorRegistered(msg.sender, blsPublicKey);
}
Expand All @@ -224,6 +244,10 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {

_validatorsData[msg.sender].blsPublicKey = blsPublicKey;

if (_canBecomeActiveValidator(msg.sender) && !_isActiveValidator(msg.sender)) {
_addActiveValidator(msg.sender);
}

emit ValidatorUpdated(msg.sender, blsPublicKey);
}

Expand All @@ -237,13 +261,25 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
revert ValidatorAlreadyResigned();
}

if (_validatorsCount - _resignedValidatorsCount <= _minValidators) {
if (_activeValidators.length <= _minValidators) {
revert BellowMinValidators();
}

validator.isResigned = true;
_resignedValidatorsCount += 1;

_removeActiveValidator(msg.sender);

// Refund the registration fee to the validator
if (validator.fee > 0) {
uint256 refundFee = validator.fee;
validator.fee = 0;
(bool success,) = payable(msg.sender).call{value: refundFee}("");
if (!success) {
revert RefundFailed();
}
}

emit ValidatorResigned(msg.sender);
}

Expand Down Expand Up @@ -305,18 +341,18 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {

_minValidators = n;

_shuffle(_validators);
_shuffle();
_deleteRoundValidators();

_roundValidatorsHead = address(0);
uint8 top = uint8(_clamp(n, 0, _validatorsCount - _resignedValidatorsCount));
uint8 top = uint8(_clamp(n, 0, _activeValidators.length));

if (top == 0) {
revert NoActiveValidators();
}

for (uint256 i = 0; i < _validators.length; i++) {
address addr = _validators[i];
for (uint256 i = 0; i < _activeValidators.length; i++) {
address addr = _activeValidators[i];

ValidatorData storage data = _validatorsData[addr];

Expand Down Expand Up @@ -375,8 +411,16 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
return 1;
}

function registeredValidatorsCount() external view returns (uint256) {
return _validatorsCount;
function fee() external view returns (uint256) {
return _fee;
}

function validatorsCount() external view returns (uint256) {
return _validators.length;
}

function activeValidatorsCount() external view returns (uint256) {
return _activeValidators.length;
}

function resignedValidatorsCount() external view returns (uint256) {
Expand Down Expand Up @@ -475,8 +519,8 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
}

// Internal functions
function _shuffle(address[] storage array) internal {
uint256 n = array.length;
function _shuffle() internal {
uint256 n = _activeValidators.length;
if (n == 0) {
return;
}
Expand All @@ -485,10 +529,35 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
// Get a random index between 0 and i (inclusive)
uint256 j = uint256(keccak256(abi.encodePacked(block.timestamp, i))) % (i + 1);

if (i == j) {
continue; // No need to swap if indices are the same
}

/* Swap example
i = 0; j = 2;

Initial state
A B C
A:0 B:1 C:2

Array SWAP
C B A
A:0 B:1 C:2

Index SWAP
C B A
A:2 B:1 C:0
*/

// Swap elements at index i and j
address temp = array[i];
array[i] = array[j];
array[j] = temp;
address addrA = _activeValidators[i];
address addrB = _activeValidators[j];

_activeValidators[i] = _activeValidators[j];
_activeValidators[j] = addrA;

_activeValidatorIndex[addrA] = j;
_activeValidatorIndex[addrB] = i;
}
}

Expand Down Expand Up @@ -574,6 +643,47 @@ contract ConsensusV1 is UUPSUpgradeable, OwnableUpgradeable {
_roundValidatorsCount++;
}

function _addActiveValidator(address addr) internal {
_activeValidators.push(addr);
_activeValidatorIndex[addr] = _activeValidators.length - 1;
}

function _removeActiveValidator(address addr) internal {
if (!_isActiveValidator(addr)) {
return;
}

uint256 index = _activeValidatorIndex[addr];
uint256 lastIndex = _activeValidators.length - 1;

// Copy last address to the index of the removed address. This is not swap. Last validator occurs 2 times in the array.
if (index != lastIndex) {
address lastValidator = _activeValidators[lastIndex];
_activeValidators[index] = lastValidator;
_activeValidatorIndex[lastValidator] = index;
}

// Remove last address
_activeValidators.pop();
delete _activeValidatorIndex[addr];
}

function _canBecomeActiveValidator(address addr) internal view returns (bool) {
ValidatorData storage data = _validatorsData[addr];
return !data.isResigned && data.blsPublicKey.length != 0;
Comment thread
sebastijankuzner marked this conversation as resolved.
}

function _isActiveValidator(address addr) internal view returns (bool) {
Comment thread
oXtxNt9U marked this conversation as resolved.
uint256 index = _activeValidatorIndex[addr];
// Support for empty array
if (index >= _activeValidators.length) {
return false;
}

// Check if the address at the index matches the address. Required for the case when index is 0.
return _activeValidators[index] == addr;
}

function _unvote() internal returns (address) {
Vote storage voter = _voters[msg.sender];
if (voter.validator == address(0)) {
Expand Down
2 changes: 1 addition & 1 deletion contracts/test/consensus/Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract Base is Test {
function test() public {}

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
bytes memory data = abi.encode(ConsensusV1.initialize.selector, 0);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}
Expand Down
4 changes: 2 additions & 2 deletions contracts/test/consensus/Consensus-CalculateTop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ contract ConsensusTest is Base {

function test_consensus_sortedValidators_sameVoteCounts() public {
vm.pauseGasMetering();
assertEq(consensus.registeredValidatorsCount(), 0);
assertEq(consensus.validatorsCount(), 0);

uint256 n = 55;
uint256 balance = 50;
Expand Down Expand Up @@ -140,7 +140,7 @@ contract ConsensusTest is Base {

function test_consensus_200_topValidators() public {
vm.pauseGasMetering();
assertEq(consensus.registeredValidatorsCount(), 0);
assertEq(consensus.validatorsCount(), 0);

address highest = address(0);
uint256 highestBalance = 0;
Expand Down
33 changes: 33 additions & 0 deletions contracts/test/consensus/Consensus-Fee.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.13;

import {ConsensusV1} from "@contracts/consensus/ConsensusV1.sol";
import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract ConsensusTest is Base {
function test_default_fee() public view {
assertEq(consensus.fee(), 0);
}

function test_default_fee_custom() public {
uint256 initialFee = 10;
bytes memory data = abi.encodeWithSelector(ConsensusV1.initialize.selector, initialFee);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
ConsensusV1 consensusCustom = ConsensusV1(proxy);

assertEq(consensusCustom.fee(), initialFee);
}

function test_default_fee_should_be_adjustable() public {
assertEq(consensus.fee(), 0);

uint128 newFee = 1000;
vm.expectEmit(address(consensus));
emit ConsensusV1.FeeUpdated(newFee);
consensus.setFee(newFee);

assertEq(consensus.fee(), newFee);
}
}
2 changes: 1 addition & 1 deletion contracts/test/consensus/Consensus-GetAllValidators.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.s
contract ConsensusTest is Base {
function test_200_validators() public {
vm.pauseGasMetering();
assertEq(consensus.registeredValidatorsCount(), 0);
assertEq(consensus.validatorsCount(), 0);

uint256 n = 200;
for (uint256 i = 0; i < n; i++) {
Expand Down
Loading
Loading