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

Implement Finalizable in GrandpaService #678

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
109 changes: 98 additions & 11 deletions src/main/java/com/limechain/grandpa/GrandpaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.limechain.exception.grandpa.GhostExecutionException;
import com.limechain.exception.storage.BlockStorageGenericException;
import com.limechain.grandpa.state.RoundState;
import com.limechain.network.protocol.grandpa.messages.catchup.res.SignedVote;
import com.limechain.network.protocol.grandpa.messages.commit.Vote;
import com.limechain.network.protocol.grandpa.messages.vote.Subround;
import com.limechain.network.protocol.warp.dto.BlockHeader;
Expand Down Expand Up @@ -30,27 +31,102 @@ public GrandpaService(RoundState roundState, BlockState blockState) {
this.blockState = blockState;
}

/**
* Determines if the specified round can be finalized.
* 1) Checks for a valid preVote candidate and ensures it's completable.
* 2) Retrieves the best final candidate for the current round, archives it,
* and compares it to the previous round’s candidate.
*
* @param roundNumber
* @return if given round is finalizable
*/
private boolean isFinalizable(BigInteger roundNumber) {
Vote preVoteCandidate = getGrandpaGhost();
if (preVoteCandidate == null) {
return false;
}

if (!isCompletable(preVoteCandidate)) {
return false;
}

Vote bestFinalCandidate = getBestFinalCandidate();
if (bestFinalCandidate == null) {
return false;
}

roundState.addBestFinalCandidateToArchive(roundNumber, bestFinalCandidate);

var prevRoundNumber = roundNumber.subtract(BigInteger.ONE);
Vote previousBestFinalCandidate = roundState.getBestFinalCandidateForRound(prevRoundNumber);

return previousBestFinalCandidate != null
&& previousBestFinalCandidate.getBlockNumber().compareTo(bestFinalCandidate.getBlockNumber()) <= 0
&& bestFinalCandidate.getBlockNumber().compareTo(preVoteCandidate.getBlockNumber()) <= 0;
}

/**
* To decide if a round is completable, we need two calculations
* 1. [TotalPcVotes + TotalPcEquivocations > 2/3 * totalValidators]
* 2. [TotalPcVotes - TotalPcEquivocations - (Votes where B` > Ghost) > 2/3 * totalValidators]
* Second calculation should be done for all Ghost descendants
*
* @param preVoteCandidate Ghost Vote
* @return if the current round is completable
*/
private boolean isCompletable(Vote preVoteCandidate) {

Map<Vote, Long> votes = getDirectVotes(Subround.PRECOMMIT);
long votesCount = votes.values().stream()
.mapToLong(Long::longValue)
.sum();

long equivocationsCount = roundState.getPcEquivocationsCount();
long totalVoters = roundState.getVoters().size();
long threshold = (2 * totalVoters) / 3;

if (votesCount + equivocationsCount <= threshold) {
return false;
}

List<Vote> ghostDescendents = getBlockDescendents(preVoteCandidate, new ArrayList<>(votes.keySet()));

for (Vote vote : ghostDescendents) {
var currentBlockHash = vote.getBlockHash();
var observedVotesForCurrentBlock = getObservedVotesForBlock(currentBlockHash, Subround.PRECOMMIT);

if (votesCount - equivocationsCount - observedVotesForCurrentBlock <= threshold) {
return false;
}
}

return true;
}

/**
* Finds and returns the best final candidate block for the current round.
* The best final candidate is determined by analyzing blocks with more than 2/3 pre-commit votes,
* and selecting the block with the highest block number. If no such block exists, the pre-voted
* block is returned as the best candidate.
*
* @param preVoteCandidate - can be null, added just for optimization
* @return the best final candidate block
*/
public Vote getBestFinalCandidate() {
public Vote getBestFinalCandidate(Vote preVoteCandidate) {

Vote prevoteCandidate = getGrandpaGhost();
if (preVoteCandidate == null) {
preVoteCandidate = getGrandpaGhost();
}

if (roundState.getRoundNumber().equals(BigInteger.ZERO)) {
return prevoteCandidate;
return preVoteCandidate;
}

var threshold = roundState.getThreshold();
Map<Hash256, BigInteger> possibleSelectedBlocks = getPossibleSelectedBlocks(threshold, Subround.PRECOMMIT);

if (possibleSelectedBlocks.isEmpty()) {
return prevoteCandidate;
return preVoteCandidate;
}

var bestFinalCandidate = getLastFinalizedBlockAsVote();
Expand All @@ -60,13 +136,13 @@ public Vote getBestFinalCandidate() {
var blockHash = block.getKey();
var blockNumber = block.getValue();

boolean isDescendant = blockState.isDescendantOf(blockHash, prevoteCandidate.getBlockHash());
boolean isDescendant = blockState.isDescendantOf(blockHash, preVoteCandidate.getBlockHash());

if (!isDescendant) {

Hash256 lowestCommonAncestor;
try {
lowestCommonAncestor = blockState.lowestCommonAncestor(blockHash, prevoteCandidate.getBlockHash());
lowestCommonAncestor = blockState.lowestCommonAncestor(blockHash, preVoteCandidate.getBlockHash());
} catch (IllegalArgumentException e) {
log.warning("Error finding the lowest common ancestor: " + e.getMessage());
continue;
Expand All @@ -88,6 +164,10 @@ public Vote getBestFinalCandidate() {
return bestFinalCandidate;
}

public Vote getBestFinalCandidate() {
return this.getBestFinalCandidate(null);
}

/**
* Finds and returns the block with the most votes in the GRANDPA prevote stage.
* If there are multiple blocks with the same number of votes, selects the block with the highest number.
Expand Down Expand Up @@ -234,9 +314,9 @@ private long getTotalVotesForBlock(Hash256 blockHash, Subround subround) {
return 0L;
}

int equivocationCount = switch (subround) {
case Subround.PREVOTE -> roundState.getPvEquivocations().size();
case Subround.PRECOMMIT -> roundState.getPcEquivocations().size();
long equivocationCount = switch (subround) {
case Subround.PREVOTE -> roundState.getPvEquivocationsCount();
case Subround.PRECOMMIT -> roundState.getPcEquivocationsCount();
default -> 0;
};

Expand Down Expand Up @@ -276,13 +356,14 @@ private long getObservedVotesForBlock(Hash256 blockHash, Subround subround) {
private HashMap<Vote, Long> getDirectVotes(Subround subround) {
var voteCounts = new HashMap<Vote, Long>();

Map<PubKey, Vote> votes = switch (subround) {
//TODO: Check in gossamer what happens when the prevote is primary proposal
Map<PubKey, SignedVote> signedVotes = switch (subround) {
case Subround.PREVOTE -> roundState.getPrevotes();
case Subround.PRECOMMIT -> roundState.getPrecommits();
default -> new HashMap<>();
};

votes.values().forEach(vote -> voteCounts.merge(vote, 1L, Long::sum));
signedVotes.values().forEach(sv -> voteCounts.merge(sv.getVote(), 1L, Long::sum));

return voteCounts;
}
Expand All @@ -292,6 +373,12 @@ private List<Vote> getVotes(Subround subround) {
return new ArrayList<>(votes.keySet());
}

private List<Vote> getBlockDescendents(Vote vote, List<Vote> votes) {
return votes.stream()
.filter(v -> v.getBlockNumber().compareTo(vote.getBlockNumber()) > 0)
.toList();
}

private Vote getLastFinalizedBlockAsVote() {
var lastFinalizedBlockHeader = blockState.getHighestFinalizedHeader();

Expand Down
39 changes: 34 additions & 5 deletions src/main/java/com/limechain/grandpa/state/RoundState.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.stereotype.Component;

import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -29,11 +30,15 @@ public class RoundState {
private BigInteger setId;
private BigInteger roundNumber;

//TODO: This may not be the best place for those maps
private Map<PubKey, Vote> precommits = new ConcurrentHashMap<>();
private Map<PubKey, Vote> prevotes = new ConcurrentHashMap<>();
private Map<PubKey, SignedVote> pvEquivocations = new ConcurrentHashMap<>();
private Map<PubKey, SignedVote> pcEquivocations = new ConcurrentHashMap<>();
private Map<PubKey, SignedVote> precommits = new ConcurrentHashMap<>();
private Map<PubKey, SignedVote> prevotes = new ConcurrentHashMap<>();

private Map<PubKey, List<SignedVote>> pvEquivocations = new ConcurrentHashMap<>();
private Map<PubKey, List<SignedVote>> pcEquivocations = new ConcurrentHashMap<>();

//TODO: Refactor if these maps are accessed/modified concurrently
private Map<BigInteger, Vote> preVotedBlocksArchive = new HashMap<>();
private Map<BigInteger, Vote> bestFinalCandidateArchive = new HashMap<>();

/**
* The threshold is determined as the total weight of authorities
Expand All @@ -57,4 +62,28 @@ public BigInteger derivePrimary() {
var votersCount = BigInteger.valueOf(voters.size());
return roundNumber.remainder(votersCount);
}

public void addPreVotedBlockToArchive(BigInteger roundNumber, Vote vote) {
this.preVotedBlocksArchive.put(roundNumber, vote);
}

public void addBestFinalCandidateToArchive(BigInteger roundNumber, Vote vote) {
this.bestFinalCandidateArchive.put(roundNumber, vote);
}

public Vote getBestFinalCandidateForRound(BigInteger roundNumber) {
return this.bestFinalCandidateArchive.get(roundNumber);
}

public long getPvEquivocationsCount() {
return this.pvEquivocations.values().stream()
.mapToLong(List::size)
.sum();
}

public long getPcEquivocationsCount() {
return this.pcEquivocations.values().stream()
.mapToInt(List::size)
.sum();
}
}
Loading
Loading