Skip to content

Commit

Permalink
Implement GRANDPA equivocation (#747)
Browse files Browse the repository at this point in the history
Implement GRANDPA equivocation
  • Loading branch information
osrib authored Feb 6, 2025
1 parent 28ad203 commit bc0e0e0
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 28 deletions.
4 changes: 2 additions & 2 deletions src/main/java/com/limechain/babe/BlockProductionVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,10 @@ private boolean isBlockEquivocationExist(byte[] authorityPublicKey,
blockEquivocationProof.setFirstBlockHeader(firstBlockHeader);
blockEquivocationProof.setSecondBlockHeader(blockHeader);

Optional<OpaqueKeyOwnershipProof> opaqueKeyOwnershipProof = runtime.generateKeyOwnershipProof(
Optional<OpaqueKeyOwnershipProof> opaqueKeyOwnershipProof = runtime.generateBabeKeyOwnershipProof(
currentSlotNumber, authorityPublicKey);
opaqueKeyOwnershipProof.ifPresentOrElse(
key -> runtime.submitReportEquivocationUnsignedExtrinsic(blockEquivocationProof, key.getProof()),
key -> runtime.submitReportBabeEquivocationUnsignedExtrinsic(blockEquivocationProof, key.getProof()),
() -> log.warning(String.format(
"Failure to report equivocation for authority: %s. Authorship verification marked as failure.",
hexPublicKey)));
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.babe.api.OpaqueKeyOwnershipProof;
import com.limechain.chain.lightsyncstate.Authority;
import com.limechain.exception.grandpa.GrandpaGenericException;
import com.limechain.exception.sync.JustificationVerificationException;
Expand All @@ -18,6 +19,7 @@
import com.limechain.network.protocol.grandpa.messages.neighbour.NeighbourMessage;
import com.limechain.network.protocol.grandpa.messages.vote.FullVote;
import com.limechain.network.protocol.grandpa.messages.vote.FullVoteScaleWriter;
import com.limechain.network.protocol.grandpa.messages.vote.GrandpaEquivocation;
import com.limechain.network.protocol.grandpa.messages.vote.SignedMessage;
import com.limechain.network.protocol.grandpa.messages.vote.VoteMessage;
import com.limechain.network.protocol.sync.BlockRequestField;
Expand All @@ -27,6 +29,7 @@
import com.limechain.network.protocol.warp.dto.Justification;
import com.limechain.network.protocol.warp.scale.reader.BlockHeaderReader;
import com.limechain.network.protocol.warp.scale.reader.JustificationReader;
import com.limechain.runtime.Runtime;
import com.limechain.runtime.hostapi.dto.Key;
import com.limechain.runtime.hostapi.dto.VerifySignature;
import com.limechain.state.AbstractState;
Expand Down Expand Up @@ -58,6 +61,8 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.limechain.grandpa.vote.SubRound.PRE_COMMIT;

@Log
@RequiredArgsConstructor
@Component
Expand Down Expand Up @@ -97,6 +102,14 @@ public void handleVoteMessage(VoteMessage voteMessage) {
GrandpaRound round = roundCache.getRound(voteMessageSetId, voteMessageRoundNumber);

SubRound subround = signedMessage.getStage();

if (isVoteEquivocationExist(signedVote, round, subround, voteMessageSetId)) {
log.warning(
String.format("Detected vote equivocation for round %s, set %s, block hash %s, block number %s",
voteMessageSetId, voteMessageSetId, signedMessage.getBlockHash(), signedMessage.getBlockNumber()));
return;
}

switch (subround) {
case PRE_VOTE -> round.getPreVotes().put(signedMessage.getAuthorityPublicKey(), signedVote);
case PRE_COMMIT -> round.getPreCommits().put(signedMessage.getAuthorityPublicKey(), signedVote);
Expand All @@ -108,6 +121,63 @@ public void handleVoteMessage(VoteMessage voteMessage) {
}
}

private boolean isValidMessageSignature(VoteMessage voteMessage) {
SignedMessage signedMessage = voteMessage.getMessage();

FullVote fullVote = new FullVote();
fullVote.setRound(voteMessage.getRound());
fullVote.setSetId(voteMessage.getSetId());
fullVote.setVote(new Vote(signedMessage.getBlockHash(), signedMessage.getBlockNumber()));
fullVote.setStage(signedMessage.getStage());

byte[] encodedFullVote = ScaleUtils.Encode.encode(FullVoteScaleWriter.getInstance(), fullVote);

VerifySignature verifySignature = new VerifySignature(
signedMessage.getSignature().getBytes(),
encodedFullVote,
signedMessage.getAuthorityPublicKey().getBytes(),
Key.ED25519);

return Ed25519Utils.verifySignature(verifySignature);
}

private boolean isVoteEquivocationExist(SignedVote signedVote, GrandpaRound round, SubRound subRound, BigInteger voteMessageSetId) {
Map<Hash256, SignedVote> votes = PRE_COMMIT.equals(subRound) ? round.getPreCommits() : round.getPreVotes();
Map<Hash256, List<SignedVote>> equivocations = PRE_COMMIT.equals(subRound) ? round.getPcEquivocations() : round.getPvEquivocations();

Hash256 authorityPublicKey = signedVote.getAuthorityPublicKey();

if (votes.containsKey(authorityPublicKey)) {
equivocations.computeIfAbsent(authorityPublicKey, _ -> new ArrayList<>()).add(signedVote);
BlockState blockState = stateManager.getBlockState();
Runtime runtime = blockState.getRuntime(blockState.getHighestFinalizedHash());
SignedVote firstSignedVote = votes.get(authorityPublicKey);

GrandpaEquivocation grandpaEquivocation =
GrandpaEquivocation.builder().
setId(voteMessageSetId).
equivocationStage((byte) (PRE_COMMIT.equals(subRound) ? 1 : 0)).
roundNumber(round.getRoundNumber()).
firstBlockNumber(firstSignedVote.getVote().getBlockNumber()).
firstBlockHash(firstSignedVote.getVote().getBlockHash()).
firstSignature(firstSignedVote.getSignature()).
secondBlockNumber(signedVote.getVote().getBlockNumber()).
secondBlockHash(signedVote.getVote().getBlockHash()).
secondSignature(signedVote.getSignature())
.build();

Optional<OpaqueKeyOwnershipProof> opaqueKeyOwnershipProof = runtime.generateGrandpaKeyOwnershipProof(voteMessageSetId, authorityPublicKey.getBytes());
opaqueKeyOwnershipProof.ifPresentOrElse(
key -> runtime.submitReportGrandpaEquivocationUnsignedExtrinsic(grandpaEquivocation, key.getProof()),
() -> log.warning(String.format(
"Failure to report Grandpa vote equivocation for authority: %s.",
authorityPublicKey)));
return true;
}

return false;
}

/**
* Updates the Host's state with information from a commit message.
* Synchronized to avoid race condition between checking and updating latest block
Expand Down Expand Up @@ -354,26 +424,6 @@ private void setVotesAndEquivocations(GrandpaRound grandpaRound,
setEquivocations.accept(grandpaRound, equivocations);
}

private boolean isValidMessageSignature(VoteMessage voteMessage) {
SignedMessage signedMessage = voteMessage.getMessage();

FullVote fullVote = new FullVote();
fullVote.setRound(voteMessage.getRound());
fullVote.setSetId(voteMessage.getSetId());
fullVote.setVote(new Vote(signedMessage.getBlockHash(), signedMessage.getBlockNumber()));
fullVote.setStage(signedMessage.getStage());

byte[] encodedFullVote = ScaleUtils.Encode.encode(FullVoteScaleWriter.getInstance(), fullVote);

VerifySignature verifySignature = new VerifySignature(
signedMessage.getSignature().getBytes(),
encodedFullVote,
signedMessage.getAuthorityPublicKey().getBytes(),
Key.ED25519);

return Ed25519Utils.verifySignature(verifySignature);
}

private void updateSyncStateAndRuntime(CommitMessage commitMessage) {
SyncState syncState = stateManager.getSyncState();
BigInteger lastFinalizedBlockNumber = syncState.getLastFinalizedBlockNumber();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.limechain.network.protocol.grandpa.messages.vote;

import io.emeraldpay.polkaj.types.Hash256;
import io.emeraldpay.polkaj.types.Hash512;
import lombok.Builder;
import lombok.Getter;

import java.math.BigInteger;

@Getter
@Builder
public class GrandpaEquivocation {
private BigInteger setId;
private byte equivocationStage;
private BigInteger roundNumber;
private Hash256 authorityPublicKey;
private Hash256 firstBlockHash;
private BigInteger firstBlockNumber;
private Hash512 firstSignature;
private Hash256 secondBlockHash;
private BigInteger secondBlockNumber;
private Hash512 secondSignature;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.limechain.network.protocol.grandpa.messages.vote;

import io.emeraldpay.polkaj.scale.ScaleCodecWriter;
import io.emeraldpay.polkaj.scale.ScaleWriter;
import io.emeraldpay.polkaj.scale.writer.UInt64Writer;

import java.io.IOException;

public class GrandpaEquivocationScaleWriter implements ScaleWriter<GrandpaEquivocation> {

private static final GrandpaEquivocationScaleWriter INSTANCE = new GrandpaEquivocationScaleWriter();

private final UInt64Writer uint64Writer;

private GrandpaEquivocationScaleWriter() {
this.uint64Writer = new UInt64Writer();
}

public static GrandpaEquivocationScaleWriter getInstance() {
return INSTANCE;
}

@Override
public void write(ScaleCodecWriter writer, GrandpaEquivocation grandpaEquivocation) throws IOException {
uint64Writer.write(writer, grandpaEquivocation.getSetId());
writer.writeByte(grandpaEquivocation.getEquivocationStage());
uint64Writer.write(writer, grandpaEquivocation.getRoundNumber());
writer.writeByteArray(grandpaEquivocation.getAuthorityPublicKey().getBytes());
uint64Writer.write(writer, grandpaEquivocation.getFirstBlockNumber());
writer.writeByteArray(grandpaEquivocation.getFirstBlockHash().getBytes());
writer.writeByteArray(grandpaEquivocation.getFirstSignature().getBytes());
uint64Writer.write(writer, grandpaEquivocation.getSecondBlockNumber());
writer.writeByteArray(grandpaEquivocation.getSecondBlockHash().getBytes());
writer.writeByteArray(grandpaEquivocation.getSecondSignature().getBytes());
}
}
9 changes: 7 additions & 2 deletions src/main/java/com/limechain/runtime/Runtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.limechain.babe.api.BlockEquivocationProof;
import com.limechain.babe.api.OpaqueKeyOwnershipProof;
import com.limechain.chain.lightsyncstate.Authority;
import com.limechain.network.protocol.grandpa.messages.vote.GrandpaEquivocation;
import com.limechain.network.protocol.warp.dto.Block;
import com.limechain.network.protocol.warp.dto.BlockHeader;
import com.limechain.rpc.methods.author.dto.DecodedKey;
Expand All @@ -23,9 +24,13 @@ public interface Runtime {

BabeApiConfiguration getBabeApiConfiguration();

Optional<OpaqueKeyOwnershipProof> generateKeyOwnershipProof(BigInteger slotNumber, byte[] authorityPublicKey);
Optional<OpaqueKeyOwnershipProof> generateBabeKeyOwnershipProof(BigInteger slotNumber, byte[] authorityPublicKey);

void submitReportEquivocationUnsignedExtrinsic(BlockEquivocationProof blockEquivocationProof, byte[] keyOwnershipProof);
void submitReportBabeEquivocationUnsignedExtrinsic(BlockEquivocationProof blockEquivocationProof, byte[] keyOwnershipProof);

Optional<OpaqueKeyOwnershipProof> generateGrandpaKeyOwnershipProof(BigInteger authoritySetId, byte[] authorityPublicKey);

void submitReportGrandpaEquivocationUnsignedExtrinsic(GrandpaEquivocation grandpaEquivocation, byte[] keyOwnershipProof);

List<DecodedKey> decodeSessionKeys(String sessionKeys);

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/limechain/runtime/RuntimeEndpoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public enum RuntimeEndpoint {
SESSION_KEYS_DECODE_SESSION_KEYS("SessionKeys_decode_session_keys"),
TRANSACTION_QUEUE_VALIDATE_TRANSACTION("TaggedTransactionQueue_validate_transaction"),
GRANDPA_API_GRANDPA_AUTHORITIES("GrandpaApi_grandpa_authorities"),
GRANDPA_API_GENERATE_KEY_OWNERSHIP_PROOF("BabeApi_generate_key_ownership_proof"),
GRANDPA_API_SUBMIT_REPORT_EQUIVOCATION_UNSIGNED_EXTRINSIC("GrandpaApi_submit_report_equivocation_unsigned_extrinsic"),
;

private final String name;
Expand Down
32 changes: 28 additions & 4 deletions src/main/java/com/limechain/runtime/RuntimeImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import com.limechain.chain.lightsyncstate.Authority;
import com.limechain.chain.lightsyncstate.scale.AuthorityReader;
import com.limechain.exception.scale.ScaleEncodingException;
import com.limechain.network.protocol.grandpa.messages.vote.GrandpaEquivocation;
import com.limechain.network.protocol.blockannounce.scale.BlockHeaderScaleWriter;
import com.limechain.network.protocol.grandpa.messages.vote.GrandpaEquivocationScaleWriter;
import com.limechain.network.protocol.transaction.scale.TransactionReader;
import com.limechain.network.protocol.warp.dto.Block;
import com.limechain.network.protocol.warp.dto.BlockHeader;
Expand Down Expand Up @@ -70,17 +72,17 @@ public BabeApiConfiguration getBabeApiConfiguration() {
}

@Override
public Optional<OpaqueKeyOwnershipProof> generateKeyOwnershipProof(BigInteger slotNumber,
byte[] authorityPublicKey) {
public Optional<OpaqueKeyOwnershipProof> generateBabeKeyOwnershipProof(BigInteger slotNumber,
byte[] authorityPublicKey) {
byte[] encodedProof = ArrayUtils.addAll(ScaleUtils.Encode.encode(
new UInt64Writer(), slotNumber), authorityPublicKey);
byte[] encodedResponse = call(RuntimeEndpoint.BABE_API_GENERATE_KEY_OWNERSHIP_PROOF, encodedProof);
return new ScaleCodecReader(encodedResponse).readOptional(OpaqueKeyOwnershipProofReader.getInstance());
}

@Override
public void submitReportEquivocationUnsignedExtrinsic(BlockEquivocationProof blockEquivocationProof,
byte[] keyOwnershipProof) {
public void submitReportBabeEquivocationUnsignedExtrinsic(BlockEquivocationProof blockEquivocationProof,
byte[] keyOwnershipProof) {
try (ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ScaleCodecWriter scaleCodecWriter = new ScaleCodecWriter(buffer)) {
BlockEquivocationProofWriter.getInstance().write(scaleCodecWriter, blockEquivocationProof);
Expand All @@ -91,6 +93,28 @@ public void submitReportEquivocationUnsignedExtrinsic(BlockEquivocationProof blo
}
}

@Override
public Optional<OpaqueKeyOwnershipProof> generateGrandpaKeyOwnershipProof(BigInteger authoritySetId,
byte[] authorityPublicKey) {
byte[] encodedProof = ArrayUtils.addAll(ScaleUtils.Encode.encode(
new UInt64Writer(), authoritySetId), authorityPublicKey);
byte[] encodedResponse = call(RuntimeEndpoint.GRANDPA_API_GENERATE_KEY_OWNERSHIP_PROOF, encodedProof);
return new ScaleCodecReader(encodedResponse).readOptional(OpaqueKeyOwnershipProofReader.getInstance());
}

@Override
public void submitReportGrandpaEquivocationUnsignedExtrinsic(GrandpaEquivocation grandpaEquivocation,
byte[] keyOwnershipProof) {
try (ByteArrayOutputStream buffer = new ByteArrayOutputStream();
ScaleCodecWriter scaleCodecWriter = new ScaleCodecWriter(buffer)) {
GrandpaEquivocationScaleWriter.getInstance().write(scaleCodecWriter, grandpaEquivocation);
scaleCodecWriter.writeAsList(keyOwnershipProof);
call(RuntimeEndpoint.GRANDPA_API_SUBMIT_REPORT_EQUIVOCATION_UNSIGNED_EXTRINSIC, buffer.toByteArray());
} catch (IOException e) {
throw new ScaleEncodingException("Unexpected exception while encoding.");
}
}

@Override
public List<DecodedKey> decodeSessionKeys(String sessionKeys) {
byte[] encodedRequest = ScaleUtils.Encode.encode(
Expand Down

0 comments on commit bc0e0e0

Please sign in to comment.