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 9, 2025
1 parent e9fae70 commit 6c8ecea
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/main/java/com/limechain/grandpa/GrandpaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.limechain.exception.storage.BlockStorageGenericException;
import com.limechain.grandpa.state.RoundState;
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;
import com.limechain.network.protocol.grandpa.messages.vote.VoteMessage;
import com.limechain.network.protocol.warp.dto.BlockHeader;
import com.limechain.storage.block.BlockState;
import io.emeraldpay.polkaj.types.Hash256;
Expand Down Expand Up @@ -111,6 +113,27 @@ public Vote getGrandpaGhost() {
return selectBlockWithMostVotes(blocks);
}

/**
* Determines what block is our pre-voted block for the current round
* 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() {
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;
}

/**
* 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
7 changes: 7 additions & 0 deletions src/main/java/com/limechain/grandpa/state/RoundState.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.limechain.chain.lightsyncstate.Authority;
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.VoteMessage;
import io.libp2p.core.crypto.PubKey;
import lombok.Getter;
import lombok.Setter;
Expand Down Expand Up @@ -34,6 +35,8 @@ public class RoundState {
private Map<PubKey, Vote> prevotes = new ConcurrentHashMap<>();
private Map<PubKey, SignedVote> pvEquivocations = new ConcurrentHashMap<>();
private Map<PubKey, SignedVote> pcEquivocations = new ConcurrentHashMap<>();
//TODO: Figure out if we need to store vote messages in round state
private VoteMessage voteMessage;

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

public void updateVoteMessage(VoteMessage voteMessage) {
this.voteMessage = voteMessage;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.limechain.network.protocol.grandpa;

import com.limechain.exception.scale.ScaleEncodingException;
import com.limechain.grandpa.state.RoundState;
import com.limechain.network.ConnectionManager;
import com.limechain.network.protocol.blockannounce.messages.BlockAnnounceHandshakeBuilder;
import com.limechain.network.protocol.grandpa.messages.GrandpaMessageType;
Expand Down Expand Up @@ -41,11 +42,13 @@ public class GrandpaEngine {
protected ConnectionManager connectionManager;
protected WarpSyncState warpSyncState;
protected BlockAnnounceHandshakeBuilder handshakeBuilder;
protected RoundState roundState;

public GrandpaEngine() {
connectionManager = ConnectionManager.getInstance();
warpSyncState = AppBean.getBean(WarpSyncState.class);
handshakeBuilder = new BlockAnnounceHandshakeBuilder();
roundState = AppBean.getBean(RoundState.class);
}

/**
Expand Down Expand Up @@ -140,6 +143,8 @@ private void handleNeighbourMessage(byte[] message, PeerId peerId) {
private void handleVoteMessage(byte[] message, PeerId peerId) {
ScaleCodecReader reader = new ScaleCodecReader(message);
VoteMessage voteMessage = reader.read(VoteMessageScaleReader.getInstance());
//Maybe we need to add possible roundNumber check
roundState.updateVoteMessage(voteMessage);
//todo: handle vote message (authoring node responsibility?)
log.log(Level.INFO, "Received vote message from Peer " + peerId + "\n" + voteMessage);
}
Expand Down
93 changes: 93 additions & 0 deletions src/test/java/com/limechain/grandpa/GrandpaServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
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.SignedMessage;
import com.limechain.network.protocol.grandpa.messages.vote.Subround;
import com.limechain.network.protocol.grandpa.messages.vote.VoteMessage;
import com.limechain.network.protocol.warp.dto.BlockHeader;
import com.limechain.network.protocol.warp.dto.ConsensusEngine;
import com.limechain.network.protocol.warp.dto.DigestType;
Expand Down Expand Up @@ -49,6 +51,7 @@ void setUp() {
roundState = mock(RoundState.class);
blockState = mock(BlockState.class);
grandpaService = new GrandpaService(roundState, blockState);

}

@Test
Expand Down Expand Up @@ -157,6 +160,96 @@ void testGetBestFinalCandidateWhereRoundNumberIsZero() {
assertEquals(blockHeader.getBlockNumber(), result.getBlockNumber());
}

@Test
void testGetBestPreVoteCandidate_WithSignedMessage() {
Vote currentVote = new Vote(new Hash256(ONES_ARRAY), BigInteger.valueOf(3));
VoteMessage voteMessage = mock(VoteMessage.class);
SignedMessage signedMessage = mock(SignedMessage.class);

when(roundState.getVoteMessage()).thenReturn(voteMessage);
BlockHeader blockHeader = createBlockHeader();
blockHeader.setBlockNumber(BigInteger.valueOf(1));

when(roundState.getThreshold()).thenReturn(BigInteger.valueOf(1));
when(roundState.getRoundNumber()).thenReturn(BigInteger.valueOf(1));

when(roundState.getPrecommits()).thenReturn(Map.of());
when(roundState.getPrevotes()).thenReturn(Map.of(
Ed25519Utils.generateKeyPair().publicKey(), currentVote
));

when(blockState.getHighestFinalizedHeader()).thenReturn(blockHeader);
when(blockState.isDescendantOf(currentVote.getBlockHash(), currentVote.getBlockHash())).thenReturn(true);
when(voteMessage.getMessage()).thenReturn(signedMessage);
when(signedMessage.getBlockNumber()).thenReturn(BigInteger.valueOf(4));
when(signedMessage.getBlockHash()).thenReturn(new Hash256(TWOS_ARRAY));

Vote result = grandpaService.getBestPreVoteCandidate();


assertNotNull(result);
assertEquals(new Hash256(TWOS_ARRAY), result.getBlockHash());
assertEquals(BigInteger.valueOf(4), result.getBlockNumber());
}

@Test
void testGetBestPreVoteCandidate_WithoutSignedMessage() {
Vote currentVote = new Vote(new Hash256(ONES_ARRAY), BigInteger.valueOf(3));
VoteMessage voteMessage = mock(VoteMessage.class);

when(roundState.getVoteMessage()).thenReturn(voteMessage);
BlockHeader blockHeader = createBlockHeader();
blockHeader.setBlockNumber(BigInteger.valueOf(1));

when(roundState.getThreshold()).thenReturn(BigInteger.valueOf(1));
when(roundState.getRoundNumber()).thenReturn(BigInteger.valueOf(1));

when(roundState.getPrecommits()).thenReturn(Map.of());
when(roundState.getPrevotes()).thenReturn(Map.of(
Ed25519Utils.generateKeyPair().publicKey(), currentVote
));

when(blockState.getHighestFinalizedHeader()).thenReturn(blockHeader);
when(blockState.isDescendantOf(currentVote.getBlockHash(), currentVote.getBlockHash())).thenReturn(true);

Vote result = grandpaService.getBestPreVoteCandidate();

assertNotNull(result);
assertEquals(currentVote.getBlockHash(), result.getBlockHash());
assertEquals(currentVote.getBlockNumber(), result.getBlockNumber());
}

@Test
void testGetBestPreVoteCandidate_WithSignedMessageAndBlockNumberLessThanCurrentVote() {
Vote currentVote = new Vote(new Hash256(ONES_ARRAY), BigInteger.valueOf(4));
VoteMessage voteMessage = mock(VoteMessage.class);
SignedMessage signedMessage = mock(SignedMessage.class);

when(roundState.getVoteMessage()).thenReturn(voteMessage);
BlockHeader blockHeader = createBlockHeader();
blockHeader.setBlockNumber(BigInteger.valueOf(1));

when(roundState.getThreshold()).thenReturn(BigInteger.valueOf(1));
when(roundState.getRoundNumber()).thenReturn(BigInteger.valueOf(1));

when(roundState.getPrecommits()).thenReturn(Map.of());
when(roundState.getPrevotes()).thenReturn(Map.of(
Ed25519Utils.generateKeyPair().publicKey(), currentVote
));

when(blockState.getHighestFinalizedHeader()).thenReturn(blockHeader);
when(blockState.isDescendantOf(currentVote.getBlockHash(), currentVote.getBlockHash())).thenReturn(true);
when(voteMessage.getMessage()).thenReturn(signedMessage);
when(signedMessage.getBlockNumber()).thenReturn(BigInteger.valueOf(3));
when(signedMessage.getBlockHash()).thenReturn(new Hash256(TWOS_ARRAY));

Vote result = grandpaService.getBestPreVoteCandidate();

assertNotNull(result);
assertEquals(currentVote.getBlockHash(), result.getBlockHash());
assertEquals(currentVote.getBlockNumber(), result.getBlockNumber());
}

@Test
void testGetGrandpaGHOSTWhereNoBlocksPassThreshold() {
when(roundState.getThreshold()).thenReturn(BigInteger.valueOf(10));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.limechain.network.protocol.grandpa;

import com.limechain.grandpa.state.RoundState;
import com.limechain.network.ConnectionManager;
import com.limechain.network.dto.PeerInfo;
import com.limechain.network.protocol.blockannounce.NodeRole;
Expand Down Expand Up @@ -48,6 +49,8 @@ class GrandpaEngineTest {
@Mock
private WarpSyncState warpSyncState;
@Mock
private RoundState roundState;
@Mock
private BlockAnnounceHandshakeBuilder blockAnnounceHandshakeBuilder;

private final NeighbourMessage neighbourMessage =
Expand Down

0 comments on commit 6c8ecea

Please sign in to comment.