Skip to content

Commit

Permalink
feat(contract): store rounds on Consensus.sol contract (#747)
Browse files Browse the repository at this point in the history
* _validatorRounds is 2d array

* Rename ValidatorRound to round

* Rename to _rounds

* Add validator round

* Rename ValidatorRounds to Rounds

* Get with offset

* Round starts with one

* Test vote balance

* Fix offset calculation

* Update abi

* Create rounds iterator

* Fix parsing

* calculate roundHeight

* revert app.json

* style: resolve style guide violations

---------

Co-authored-by: oXtxNt9U <[email protected]>
Co-authored-by: oXtxNt9U <[email protected]>
  • Loading branch information
3 people authored Oct 31, 2024
1 parent dc8ab30 commit 8574a37
Show file tree
Hide file tree
Showing 11 changed files with 549 additions and 716 deletions.
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

0 comments on commit 8574a37

Please sign in to comment.