Skip to content

Commit

Permalink
feat(contract): prevent validator resignation below min validators (#766
Browse files Browse the repository at this point in the history
)

* Check min validators

* Add docs

* Set minValidators

* Use tmp array

* Shuffle temp array

* Move consensus to base

* Move setup to base

* Test resignations

* Init min validators

* Remove todo

* Update abi
  • Loading branch information
sebastijankuzner authored Nov 18, 2024
1 parent 9a4056a commit fa5bff4
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 149 deletions.
65 changes: 50 additions & 15 deletions contracts/src/consensus/ConsensusV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ error CallerIsNotValidator();
error ValidatorNotRegistered();
error ValidatorAlreadyRegistered();
error ValidatorAlreadyResigned();
error BellowMinValidators();

error BlsKeyAlreadyRegistered();
error BlsKeyIsInvalid();
Expand All @@ -64,6 +65,11 @@ error MissingVote();

error InvalidRange(uint256 min, uint256 max);

// Validators:
// - Registered -> All validators that are registered including resigned validators
// - Active -> Top N validators with the highest vote balance, that participate in the consensus
// - Resigned -> Validators that resigned from the consensus

// Voter calls vote funtion
// Vote function includes valdiator address and balance, whole balance is added to the validator voteBalance
// Voter can unvote, whole balance is removed from validator voteBalance
Expand All @@ -84,18 +90,19 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
mapping(address => bool) private _hasValidator;
mapping(bytes32 => bool) private _blsPublicKeys;
address[] private _validators;
uint256 private _validatorsCount = 0;
uint256 private _resignedValidatorsCount = 0;
uint256 private _validatorsCount; // Default 0
uint256 private _resignedValidatorsCount; // Default 0

mapping(address => Vote) private _voters;
address private _votersHead = address(0);
address private _votersTail = address(0);
uint256 private _votersCount = 0;
address private _votersHead; // Default address(0)
address private _votersTail; // Default address(0)
uint256 private _votersCount; // Default 0

mapping(address => address) private _activeValidatorsMap;
address[] private _activeValidators;
address private _activeValidatorsHead;
uint256 private _activeValidatorsCount = 0;
address private _activeValidatorsHead; // Default address(0)
uint256 private _activeValidatorsCount; // Default 0
uint256 private _minValidators; // Default 1

RoundValidator[][] private _rounds;

Expand All @@ -117,6 +124,7 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
// Initializers
function initialize() public initializer {
_owner = msg.sender;
_minValidators = 1;
}

// Overrides
Expand Down Expand Up @@ -163,6 +171,10 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
revert ValidatorAlreadyResigned();
}

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

validator.isResigned = true;
_resignedValidatorsCount += 1;

Expand Down Expand Up @@ -217,6 +229,8 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
}

function calculateActiveValidators(uint8 n) external onlyOwner {
_minValidators = n;

_shuffle(_validators);
_deleteActiveValidators();

Expand Down Expand Up @@ -257,18 +271,26 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
}
}

RoundValidator[] storage round = _rounds.push();

// Prepare temp array. Used when top < _minValidators
address next = _activeValidatorsHead;
delete _activeValidators;
_activeValidators = new address[](top);
address[] memory tmpValidators = new address[](top);

for (uint256 i = 0; i < top; i++) {
_activeValidators[i] = next;
round.push(RoundValidator({addr: next, voteBalance: _validatorsData[next].voteBalance}));
tmpValidators[i] = next;
next = _activeValidatorsMap[next];
}
_shuffleMem(tmpValidators);

// Fill round & _activeValidators
RoundValidator[] storage round = _rounds.push();
delete _activeValidators;
_activeValidators = new address[](_minValidators);

_shuffle(_activeValidators);
for (uint256 i = 0; i < _minValidators; i++) {
address addr = tmpValidators[i % top];
_activeValidators[i] = addr;
round.push(RoundValidator({addr: addr, voteBalance: _validatorsData[addr].voteBalance}));
}
}

// External functions that are view
Expand All @@ -285,7 +307,7 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
}

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

function isValidatorRegistered(address addr) public view returns (bool) {
Expand Down Expand Up @@ -389,6 +411,19 @@ contract ConsensusV1 is Initializable, UUPSUpgradeable {
}
}

function _shuffleMem(address[] memory array) internal view {
uint256 n = array.length;
for (uint256 i = n - 1; i > 0; i--) {
// Get a random index between 0 and i (inclusive)
uint256 j = uint256(keccak256(abi.encodePacked(block.timestamp, i))) % (i + 1);

// Swap elements at index i and j
address temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}

function _deleteActiveValidators() internal {
address next = _activeValidatorsHead;

Expand Down
34 changes: 33 additions & 1 deletion contracts/test/consensus/Base.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@
pragma solidity ^0.8.13;

import {Test, console} from "@forge-std/Test.sol";
import {Validator} from "@contracts/consensus/ConsensusV1.sol";
import {ConsensusV1, Validator} from "@contracts/consensus/ConsensusV1.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract Base is Test {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function prepareBLSKey(address addr, uint8 lenght) public pure returns (bytes memory) {
bytes32 h = keccak256(abi.encode(addr));
bytes memory validatorKey = new bytes(lenght);
Expand Down Expand Up @@ -40,4 +49,27 @@ contract Base is Test {

return validatorA.data.voteBalance > validatorB.data.voteBalance;
}

function registerValidator(address addr) internal {
bytes32 h = keccak256(abi.encode(addr));
bytes memory validatorKey = new bytes(48);
for (uint256 j = 0; j < 32; j++) {
validatorKey[j] = h[j];
}

vm.startPrank(addr);
consensus.registerValidator(validatorKey);
vm.stopPrank();

Validator memory validator = consensus.getValidator(addr);
assertEq(validator.addr, addr);
assertEq(validator.data.voteBalance, 0 ether);
assertEq(validator.data.votersCount, 0);
}

function resignValidator(address addr) internal {
vm.startPrank(addr);
consensus.resignValidator();
vm.stopPrank();
}
}
27 changes: 9 additions & 18 deletions contracts/test/consensus/Consensus-CalculateTop.sol
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.13;

import {Test, console} from "@forge-std/Test.sol";
import {ConsensusV1, ValidatorData, Validator, CallerIsNotOwner} from "@contracts/consensus/ConsensusV1.sol";
import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ConsensusTest is Base {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function test_should_work_with_one_validator() public {
address addr = address(1);
vm.startPrank(addr);
consensus.registerValidator(prepareBLSKey(addr));
vm.stopPrank();
registerValidator(addr);

consensus.calculateActiveValidators(1);
Validator[] memory validators = consensus.getActiveValidators();
Expand All @@ -37,14 +26,15 @@ contract ConsensusTest is Base {
function test_should_ignore_resigned_validators() public {
address addr = address(1);

vm.startPrank(addr);
consensus.registerValidator(prepareBLSKey(addr));
consensus.resignValidator();
vm.stopPrank();
registerValidator(addr);
registerValidator(address(2));
resignValidator(addr);

consensus.calculateActiveValidators(1);
consensus.calculateActiveValidators(2);
Validator[] memory validators = consensus.getActiveValidators();
assertEq(validators.length, 0);
assertEq(validators.length, 2);
assertEq(validators[0].addr, address(2));
assertEq(validators[1].addr, address(2)); // Second validator is duplicated
}

function test_consensus_200_topValidators() public {
Expand Down Expand Up @@ -90,6 +80,7 @@ contract ConsensusTest is Base {
validators = sortValidators(validators);
assertEq(validators[activeValidators - 1].addr, highest);

// Seccond attempt shoudl return the same result
consensus.calculateActiveValidators(uint8(activeValidators));

validators = consensus.getActiveValidators();
Expand Down
9 changes: 0 additions & 9 deletions contracts/test/consensus/Consensus-GetAllValidators.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.13;

import {Test, console} from "@forge-std/Test.sol";
import {ConsensusV1, ValidatorData, Validator} from "@contracts/consensus/ConsensusV1.sol";
import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ConsensusTest is Base {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function test_200_validators() public {
vm.pauseGasMetering();
assertEq(consensus.registeredValidatorsCount(), 0);
Expand Down
11 changes: 1 addition & 10 deletions contracts/test/consensus/Consensus-Proxy.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.13;

import {Test, console} from "@forge-std/Test.sol";
import {ConsensusV1, ValidatorData, Validator, CallerIsNotOwner} from "@contracts/consensus/ConsensusV1.sol";
import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
Expand All @@ -14,20 +13,12 @@ contract ConsensusVTest is ConsensusV1 {
}

contract ConsensusTest is Base {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function test_initialize_should_revert() public {
vm.expectRevert(Initializable.InvalidInitialization.selector);
consensus.initialize();
}

function test_shoudl_have_valid_UPGRADE_INTERFACE_VERSION() public {
function test_shoudl_have_valid_UPGRADE_INTERFACE_VERSION() public view {
assertEq(consensus.UPGRADE_INTERFACE_VERSION(), "5.0.0");
}

Expand Down
9 changes: 0 additions & 9 deletions contracts/test/consensus/Consensus-Rounds.sol
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.13;

import {Test, console} from "@forge-std/Test.sol";
import {ConsensusV1, Round, CallerIsNotOwner} from "@contracts/consensus/ConsensusV1.sol";
import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ConsensusTest is Base {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function test_revert_if_caller_is_not_owner() public {
vm.startPrank(address(1));

Expand Down
12 changes: 2 additions & 10 deletions contracts/test/consensus/Consensus-UpdateVoters.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: GNU GENERAL PUBLIC LICENSE
pragma solidity ^0.8.13;

import {Test, console} from "@forge-std/Test.sol";
import {
ConsensusV1,
ValidatorData,
Expand All @@ -10,17 +9,10 @@ import {
Voted,
CallerIsNotOwner
} from "@contracts/consensus/ConsensusV1.sol";
import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ConsensusTest is Test {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

contract ConsensusTest is Base {
function test_updateVoters_should_allow_only_caller() public {
address addr = address(1);
vm.startPrank(addr);
Expand Down
8 changes: 0 additions & 8 deletions contracts/test/consensus/Consensus-ValidatoUpdate.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ConsensusTest is Base {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function test_updateBlsPublicKey_revert_if_caller_is_not_validator() public {
vm.expectRevert(CallerIsNotValidator.selector);
consensus.resignValidator();
Expand Down
8 changes: 0 additions & 8 deletions contracts/test/consensus/Consensus-ValidatorRegistration.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ import {Base} from "./Base.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";

contract ConsensusTest is Base {
ConsensusV1 public consensus;

function setUp() public {
bytes memory data = abi.encode(ConsensusV1.initialize.selector);
address proxy = address(new ERC1967Proxy(address(new ConsensusV1()), data));
consensus = ConsensusV1(proxy);
}

function test_validator_registration_pass() public {
assertEq(consensus.registeredValidatorsCount(), 0);
address addr = address(1);
Expand Down
Loading

0 comments on commit fa5bff4

Please sign in to comment.