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

feat(contract): store rounds on Consensus.sol contract #747

Merged
merged 15 commits into from
Oct 31, 2024
47 changes: 28 additions & 19 deletions contracts/src/consensus/Consensus.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ struct Validator {
ValidatorData data;
}

struct RoundValidator {
address addr;
uint256 voteBalance;
}

struct Round {
uint256 round;
RoundValidator[] validators;
}

struct Vote {
address validator;
uint256 balance;
Expand All @@ -24,16 +34,6 @@ struct VoteResult {
address validator;
}

struct ValidatorRoundValidator {
address validatorAddress;
uint256 voteBalance;
}

struct ValidatorRound {
uint256 round;
ValidatorRoundValidator[] validators;
}

event ValidatorRegistered(address addr, bytes bls12_381_public_key);

event ValidatorResigned(address addr);
Expand Down Expand Up @@ -82,7 +82,7 @@ contract Consensus {
uint256 private _topValidatorsCount = 0;
address[] private _calculatedTopValidators;

ValidatorRound[] private _validatorRounds;
RoundValidator[][] private _rounds;

constructor() {
_owner = msg.sender;
Expand Down Expand Up @@ -160,13 +160,14 @@ contract Consensus {
}
}

// TODO: update _validatorRounds
RoundValidator[] storage round = _rounds.push();

address next = _topValidatorsHead;
delete _calculatedTopValidators;
_calculatedTopValidators = new address[](top);
for (uint256 i = 0; i < top; i++) {
_calculatedTopValidators[i] = next;
round.push(RoundValidator({addr: next, voteBalance: _registeredValidatorData[next].voteBalance}));
next = _topValidators[next];
}
}
Expand Down Expand Up @@ -413,13 +414,21 @@ contract Consensus {
}
}

// TODO: allow passing limit to cap maximum number of returned items in case validator count is very high.
// the caller can paginate to retrieve all items.
function getValidatorRounds() public view onlyOwner returns (ValidatorRound[] memory) {
ValidatorRound[] memory result = new ValidatorRound[](_validatorRounds.length);
for (uint256 i = 0; i < _validatorRounds.length; i++) {
ValidatorRound storage data = _validatorRounds[i];
result[i] = data;
function getRoundsCount() public view returns (uint256) {
return _rounds.length;
}

function getRounds(uint256 offset, uint256 count) public view onlyOwner returns (Round[] memory) {
uint256 total = count;
if (offset >= _rounds.length) {
total = 0;
} else if (offset + count > _rounds.length) {
total = _rounds.length - offset;
}

Round[] memory result = new Round[](total);
for (uint256 i = 0; i < total; i++) {
result[i] = Round({round: offset + i + 1, validators: _rounds[offset + i]});
}

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

import {Test, console} from "@forge-std/Test.sol";
import {Consensus, Round} from "@contracts/consensus/Consensus.sol";
import {Base} from "./Base.sol";

contract ConsensusTest is Base {
Consensus public consensus;

function setUp() public {
consensus = new Consensus();
}

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

vm.expectRevert("Caller is not the contract owner");
consensus.getRounds(0, 10);
}

function test_should_return_empty() public view {
assertEq(consensus.getRoundsCount(), 0);
assertEq(consensus.getRounds(0, 10).length, 0);
}

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

consensus.calculateTopValidators(1);

assertEq(consensus.getRoundsCount(), 1);
Round[] memory rounds = consensus.getRounds(0, 10);
assertEq(rounds.length, 1);
assertEq(rounds[0].round, 1);
assertEq(rounds[0].validators.length, 1);
assertEq(rounds[0].validators[0].addr, addr);
}

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

// Round 1
consensus.calculateTopValidators(1);
assertEq(consensus.getRoundsCount(), 1);
Round[] memory rounds = consensus.getRounds(0, 10);
assertEq(rounds.length, 1);
assertEq(rounds[0].round, 1);
assertEq(rounds[0].validators.length, 1);
assertEq(rounds[0].validators[0].addr, addr);
assertEq(rounds[0].validators[0].voteBalance, 0 ether);

// Vote
address voterAddr1 = address(2);
vm.deal(voterAddr1, 100 ether);
vm.startPrank(voterAddr1);
consensus.vote(addr);
vm.stopPrank();

// Round 2
consensus.calculateTopValidators(1);
assertEq(consensus.getRoundsCount(), 2);
rounds = consensus.getRounds(0, 10);
assertEq(rounds.length, 2);
assertEq(rounds[0].round, 1);
assertEq(rounds[0].validators.length, 1);
assertEq(rounds[0].validators[0].addr, addr);
assertEq(rounds[0].validators[0].voteBalance, 0 ether);
assertEq(rounds[1].round, 2);
assertEq(rounds[1].validators.length, 1);
assertEq(rounds[1].validators[0].addr, addr);
assertEq(rounds[1].validators[0].voteBalance, 100 ether);

// Vote
address voterAddr2 = address(3);
vm.deal(voterAddr2, 100 ether);
vm.startPrank(voterAddr2);
consensus.vote(addr);
vm.stopPrank();

// Round 3
consensus.calculateTopValidators(1);
assertEq(consensus.getRoundsCount(), 3);
rounds = consensus.getRounds(0, 10);
assertEq(rounds.length, 3);
assertEq(rounds[0].round, 1);
assertEq(rounds[0].validators.length, 1);
assertEq(rounds[0].validators[0].addr, addr);
assertEq(rounds[0].validators[0].voteBalance, 0 ether);
assertEq(rounds[1].round, 2);
assertEq(rounds[1].validators.length, 1);
assertEq(rounds[1].validators[0].addr, addr);
assertEq(rounds[1].validators[0].voteBalance, 100 ether);
assertEq(rounds[2].round, 3);
assertEq(rounds[2].validators.length, 1);
assertEq(rounds[2].validators[0].addr, addr);
assertEq(rounds[2].validators[0].voteBalance, 200 ether);
}

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

// Create 3 rounds
consensus.calculateTopValidators(1);
consensus.calculateTopValidators(1);
consensus.calculateTopValidators(1);

// Assert rounds count
assertEq(consensus.getRoundsCount(), 3);

// Shoudl slice array if count is greater than rounds left
Round[] memory rounds = consensus.getRounds(0, 10);
assertEq(rounds.length, 3);
assertEq(rounds[0].round, 1);
assertEq(rounds[1].round, 2);
assertEq(rounds[2].round, 3);

// Shoudl work with count 0
rounds = consensus.getRounds(0, 0);
assertEq(rounds.length, 0);

// Shoudl work with count 1
rounds = consensus.getRounds(0, 1);
assertEq(rounds.length, 1);
assertEq(rounds[0].round, 1);

// Shoudl respect offset
rounds = consensus.getRounds(1, 1);
assertEq(rounds.length, 1);
assertEq(rounds[0].round, 2);

rounds = consensus.getRounds(2, 1);
assertEq(rounds.length, 1);
assertEq(rounds[0].round, 3);

// Should return empty if out of range
rounds = consensus.getRounds(3, 1);
assertEq(rounds.length, 0);

rounds = consensus.getRounds(100, 100);
assertEq(rounds.length, 0);
}
}
21 changes: 0 additions & 21 deletions contracts/test/consensus/Consensus-ValidatorRounds.sol

This file was deleted.

50 changes: 29 additions & 21 deletions packages/api-sync/source/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,27 +432,11 @@ export class Restore {
const t0 = performance.now();

let totalRounds = 0;
let validatorRounds: Models.ValidatorRound[] = [];

const rounds = await this.consensusContractService.getValidatorRounds();

for (const batch of chunk(rounds, 256)) {
const validatorRounds: Models.ValidatorRound[] = [];

for (const { round, roundHeight, validators } of batch) {
const validatorAddresses: string[] = [];
const votes: string[] = [];

for (const validator of validators) {
validatorAddresses.push(validator.address);
votes.push(Utils.BigNumber.make(validator.voteBalance).toFixed());
}

validatorRounds.push({
round,
roundHeight,
validators: validatorAddresses,
votes,
});
const insert = async () => {
if (validatorRounds.length === 0) {
return;
}

await context.validatorRoundRepository
Expand All @@ -462,9 +446,33 @@ export class Restore {
.values(validatorRounds)
.execute();

totalRounds += validatorRounds.length;
validatorRounds = [];
};

for await (const { round, roundHeight, validators } of this.consensusContractService.getValidatorRounds()) {
const validatorAddresses: string[] = [];
const votes: string[] = [];

for (const validator of validators) {
validatorAddresses.push(validator.address);
votes.push(validator.voteBalance.toFixed());
}

validatorRounds.push({
round,
roundHeight,
validators: validatorAddresses,
votes,
});
totalRounds += 1;

if (validatorRounds.length === 256) {
await insert();
}
}

await insert();

const t1 = performance.now();
this.logger.info(`Restored ${totalRounds.toLocaleString()} validator rounds in ${t1 - t0}ms`);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/contracts/source/contracts/evm/contract-service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { BigNumber } from "@mainsail/utils";

import { ValidatorWallet } from "../state/wallets.js";

export interface Vote {
Expand All @@ -6,7 +8,7 @@ export interface Vote {
}

export interface ValidatorRoundValidator {
readonly voteBalance: bigint;
readonly voteBalance: BigNumber;
readonly address: string;
}

Expand All @@ -21,5 +23,5 @@ export interface ConsensusContractService {
getAllValidators(): Promise<ValidatorWallet[]>;
getVotesCount(): Promise<number>;
getVotes(): AsyncIterable<Vote>;
getValidatorRounds(): Promise<ValidatorRound[]>;
getValidatorRounds(): AsyncIterable<ValidatorRound>;
}
Loading
Loading