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

Implemented Best PreVote Candidate algorithm #679

Merged
merged 5 commits into from
Jan 17, 2025
Merged
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
101 changes: 66 additions & 35 deletions src/main/java/com/limechain/grandpa/GrandpaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

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.Subround;
import com.limechain.network.protocol.warp.dto.BlockHeader;
import com.limechain.storage.block.BlockState;
import io.emeraldpay.polkaj.types.Hash256;
import io.libp2p.core.crypto.PubKey;
import lombok.extern.java.Log;
import org.springframework.stereotype.Component;

Expand All @@ -22,11 +23,11 @@
@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 @@ -38,16 +39,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 @@ -95,14 +96,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 @@ -111,6 +112,35 @@ public Vote getGrandpaGhost() {
return selectBlockWithMostVotes(blocks);
}

/**
* Determines what block is our pre-voted block for the current round
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment explains the pseudo code of the algo from the spec, but does not reflect the code in the method.

* if we receive a vote message from the network with a
* block that's greater than or equal to the current pre-voted block
* and greater than the best final candidate from the last round, we choose that.
* otherwise, we simply choose the head of our chain.
*
* @return the best pre-voted block
*/
public Vote getBestPreVoteCandidate(GrandpaRound grandpaRound) {
Vote previousBestFinalCandidate = grandpaRound.getPrevious() != null
? grandpaRound.getPrevious().getBestFinalCandidate()
: new Vote(null, BigInteger.ZERO);
Vote currentVote = getGrandpaGhost(grandpaRound);

SignedVote primaryVote = grandpaRound.getPrimaryVote();

if (primaryVote != null) {
BigInteger primaryBlockNumber = primaryVote.getVote().getBlockNumber();

if (primaryBlockNumber.compareTo(currentVote.getBlockNumber()) > 0 &&
primaryBlockNumber.compareTo(previousBestFinalCandidate.getBlockNumber()) > 0) {
return primaryVote.getVote();
}
}
return currentVote;
}


/**
* Selects the block with the most votes from the provided map of blocks.
* If multiple blocks have the same number of votes, it returns the one with the highest block number.
Expand Down Expand Up @@ -143,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 @@ -159,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 @@ -179,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 @@ -203,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 @@ -212,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 @@ -227,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 @@ -251,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 @@ -273,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();
Map<Hash256, SignedVote> votes = switch (subround) {
case Subround.PREVOTE -> grandpaRound.getPreVotes();
case Subround.PRECOMMIT -> grandpaRound.getPreCommits();
default -> new HashMap<>();
};

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 SignedVote primaryVote;

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