Skip to content

Commit

Permalink
Implemented Best PreVote Candidate algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleksandr committed Jan 15, 2025
1 parent 2c3812f commit b9cfa61
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 153 deletions.
98 changes: 53 additions & 45 deletions src/main/java/com/limechain/grandpa/GrandpaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.limechain.exception.grandpa.GhostExecutionException;
import com.limechain.exception.storage.BlockStorageGenericException;
import com.limechain.grandpa.state.RoundState;
import com.limechain.grandpa.state.GrandpaRound;
import com.limechain.grandpa.state.GrandpaSetState;
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.SignedMessage;
import com.limechain.network.protocol.grandpa.messages.vote.Subround;
Expand All @@ -16,19 +18,21 @@

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

@Log
@Component
public class GrandpaService {

private final RoundState roundState;
private final GrandpaSetState grandpaSetState;
private final BlockState blockState;

public GrandpaService(RoundState roundState, BlockState blockState) {
this.roundState = roundState;
public GrandpaService(GrandpaSetState grandpaSetState, BlockState blockState) {
this.grandpaSetState = grandpaSetState;
this.blockState = blockState;
}

Expand All @@ -40,16 +44,16 @@ public GrandpaService(RoundState roundState, BlockState blockState) {
*
* @return the best final candidate block
*/
public Vote getBestFinalCandidate() {
public Vote getBestFinalCandidate(GrandpaRound grandpaRound) {

Vote prevoteCandidate = getGrandpaGhost();
Vote prevoteCandidate = getGrandpaGhost(grandpaRound);

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

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

if (possibleSelectedBlocks.isEmpty()) {
return prevoteCandidate;
Expand Down Expand Up @@ -97,14 +101,14 @@ public Vote getBestFinalCandidate() {
*
* @return GRANDPA GHOST block as a vote
*/
public Vote getGrandpaGhost() {
var threshold = roundState.getThreshold();
public Vote getGrandpaGhost(GrandpaRound grandpaRound) {
var threshold = grandpaSetState.getThreshold();

if (roundState.getRoundNumber().equals(BigInteger.ZERO)) {
if (grandpaRound.getRoundNumber().equals(BigInteger.ZERO)) {
return getLastFinalizedBlockAsVote();
}

Map<Hash256, BigInteger> blocks = getPossibleSelectedBlocks(threshold, Subround.PREVOTE);
Map<Hash256, BigInteger> blocks = getPossibleSelectedBlocks(grandpaRound, threshold, Subround.PREVOTE);

if (blocks.isEmpty() || threshold.equals(BigInteger.ZERO)) {
throw new GhostExecutionException("GHOST not found");
Expand All @@ -122,16 +126,19 @@ public Vote getGrandpaGhost() {
*
* @return the best pre-voted block
*/
public Vote getBestPreVoteCandidate() {
Vote currentVote = getGrandpaGhost();
VoteMessage voteMessage = roundState.getVoteMessage();
SignedMessage signedMessage = voteMessage.getMessage();

if (signedMessage != null && signedMessage.getBlockNumber().compareTo(currentVote.getBlockNumber()) > 0) {
return new Vote(signedMessage.getBlockHash(), signedMessage.getBlockNumber());
}

return currentVote;
public Vote getBestPreVoteCandidate(GrandpaRound grandpaRound) {
Vote previousBestFinalCandidate = grandpaRound.getPrevious().getBestFinalCandidate();
Vote currentVote = getGrandpaGhost(grandpaRound);

return grandpaRound.getPrimaryProposals().values().stream()
.max(Comparator.comparing(signedVote -> signedVote.getVote().getBlockNumber()))
.filter(primaryVote -> {
BigInteger primaryVoteBlockNumber = primaryVote.getVote().getBlockNumber();
return primaryVoteBlockNumber.compareTo(currentVote.getBlockNumber()) > 0 &&
primaryVoteBlockNumber.compareTo(previousBestFinalCandidate.getBlockNumber()) > 0;
})
.map(SignedVote::getVote)
.orElse(currentVote);
}

/**
Expand Down Expand Up @@ -166,12 +173,12 @@ private Vote selectBlockWithMostVotes(Map<Hash256, BigInteger> blocks) {
* @param subround stage of the GRANDPA process, such as PREVOTE, PRECOMMIT or PRIMARY_PROPOSAL.
* @return blocks that exceed the required vote threshold
*/
private Map<Hash256, BigInteger> getPossibleSelectedBlocks(BigInteger threshold, Subround subround) {
var votes = getDirectVotes(subround);
private Map<Hash256, BigInteger> getPossibleSelectedBlocks(GrandpaRound grandpaRound, BigInteger threshold, Subround subround) {
var votes = getDirectVotes(grandpaRound, subround);
var blocks = new HashMap<Hash256, BigInteger>();

for (Vote vote : votes.keySet()) {
long totalVotes = getTotalVotesForBlock(vote.getBlockHash(), subround);
long totalVotes = getTotalVotesForBlock(grandpaRound, vote.getBlockHash(), subround);

if (BigInteger.valueOf(totalVotes).compareTo(threshold) >= 0) {
blocks.put(vote.getBlockHash(), vote.getBlockNumber());
Expand All @@ -182,10 +189,10 @@ private Map<Hash256, BigInteger> getPossibleSelectedBlocks(BigInteger threshold,
return blocks;
}

List<Vote> allVotes = getVotes(subround);
List<Vote> allVotes = getVotes(grandpaRound, subround);
for (Vote vote : votes.keySet()) {
blocks = new HashMap<>(
getPossibleSelectedAncestors(allVotes, vote.getBlockHash(), blocks, subround, threshold)
getPossibleSelectedAncestors(grandpaRound, allVotes, vote.getBlockHash(), blocks, subround, threshold)
);
}

Expand All @@ -202,7 +209,8 @@ private Map<Hash256, BigInteger> getPossibleSelectedBlocks(BigInteger threshold,
* @param threshold minimum votes required for a block to qualify.
* @return map of block hash to block number for ancestors meeting the threshold condition.
*/
private Map<Hash256, BigInteger> getPossibleSelectedAncestors(List<Vote> votes,
private Map<Hash256, BigInteger> getPossibleSelectedAncestors(GrandpaRound grandpaRound,
List<Vote> votes,
Hash256 currentBlockHash,
Map<Hash256, BigInteger> selected,
Subround subround,
Expand All @@ -226,7 +234,7 @@ private Map<Hash256, BigInteger> getPossibleSelectedAncestors(List<Vote> votes,
return selected;
}

long totalVotes = getTotalVotesForBlock(ancestorBlockHash, subround);
long totalVotes = getTotalVotesForBlock(grandpaRound, ancestorBlockHash, subround);

if (BigInteger.valueOf(totalVotes).compareTo(threshold) >= 0) {

Expand All @@ -235,7 +243,7 @@ private Map<Hash256, BigInteger> getPossibleSelectedAncestors(List<Vote> votes,

} else {
// Recursively process ancestors
selected = getPossibleSelectedAncestors(votes, ancestorBlockHash, selected, subround, threshold);
selected = getPossibleSelectedAncestors(grandpaRound, votes, ancestorBlockHash, selected, subround, threshold);
}
}

Expand All @@ -250,16 +258,16 @@ private Map<Hash256, BigInteger> getPossibleSelectedAncestors(List<Vote> votes,
* @param subround stage of the GRANDPA process, such as PREVOTE, PRECOMMIT or PRIMARY_PROPOSAL.
* @return total votes for a specific block
*/
private long getTotalVotesForBlock(Hash256 blockHash, Subround subround) {
long votesForBlock = getObservedVotesForBlock(blockHash, subround);
private long getTotalVotesForBlock(GrandpaRound grandpaRound, Hash256 blockHash, Subround subround) {
long votesForBlock = getObservedVotesForBlock(grandpaRound, blockHash, subround);

if (votesForBlock == 0L) {
return 0L;
}

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

Expand All @@ -274,8 +282,8 @@ private long getTotalVotesForBlock(Hash256 blockHash, Subround subround) {
* @param subround stage of the GRANDPA process, such as PREVOTE, PRECOMMIT or PRIMARY_PROPOSAL.
* @return total observed votes
*/
private long getObservedVotesForBlock(Hash256 blockHash, Subround subround) {
var votes = getDirectVotes(subround);
private long getObservedVotesForBlock(GrandpaRound grandpaRound, Hash256 blockHash, Subround subround) {
var votes = getDirectVotes(grandpaRound, subround);
var votesForBlock = 0L;

for (Map.Entry<Vote, Long> entry : votes.entrySet()) {
Expand All @@ -296,22 +304,22 @@ private long getObservedVotesForBlock(Hash256 blockHash, Subround subround) {
* @param subround stage of the GRANDPA process, such as PREVOTE, PRECOMMIT or PRIMARY_PROPOSAL.
* @return map of direct votes
*/
private HashMap<Vote, Long> getDirectVotes(Subround subround) {
private HashMap<Vote, Long> getDirectVotes(GrandpaRound grandpaRound, Subround subround) {
var voteCounts = new HashMap<Vote, Long>();

Map<PubKey, Vote> votes = switch (subround) {
case Subround.PREVOTE -> roundState.getPrevotes();
case Subround.PRECOMMIT -> roundState.getPrecommits();
default -> new HashMap<>();
Map<Hash256, SignedVote> votes = switch (subround) {
case Subround.PREVOTE -> grandpaRound.getPreVotes();
case Subround.PRECOMMIT -> grandpaRound.getPreCommits();
case Subround.PRIMARY_PROPOSAL -> grandpaRound.getPrimaryProposals();
};

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

return voteCounts;
}

private List<Vote> getVotes(Subround subround) {
var votes = getDirectVotes(subround);
private List<Vote> getVotes(GrandpaRound grandpaRound, Subround subround) {
var votes = getDirectVotes(grandpaRound, subround);
return new ArrayList<>(votes.keySet());
}

Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/limechain/grandpa/state/GrandpaRound.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.limechain.grandpa.state;

import com.limechain.network.protocol.grandpa.messages.catchup.res.SignedVote;
import com.limechain.network.protocol.grandpa.messages.commit.Vote;
import io.emeraldpay.polkaj.types.Hash256;
import lombok.Getter;
import lombok.Setter;

import java.math.BigInteger;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Getter
@Setter
public class GrandpaRound {

private GrandpaRound previous;
private BigInteger roundNumber;

private Vote preVotedBlock;
private Vote bestFinalCandidate;

private Map<Hash256, SignedVote> preVotes = new ConcurrentHashMap<>();
private Map<Hash256, SignedVote> preCommits = new ConcurrentHashMap<>();
private Map<Hash256, SignedVote> primaryProposals = new ConcurrentHashMap<>();

private Map<Hash256, Set<SignedVote>> pvEquivocations = new ConcurrentHashMap<>();
private Map<Hash256, Set<SignedVote>> pcEquivocations = new ConcurrentHashMap<>();
}
Loading

0 comments on commit b9cfa61

Please sign in to comment.