From bc0e0e0dfe8ba45fb76f37ff2fe57ad3e0bc875f Mon Sep 17 00:00:00 2001 From: osrib Date: Thu, 6 Feb 2025 22:53:59 +0200 Subject: [PATCH] Implement GRANDPA equivocation (#747) Implement GRANDPA equivocation --- .../babe/BlockProductionVerifier.java | 4 +- .../grandpa/GrandpaMessageHandler.java | 90 ++++++++++++++----- .../messages/vote/GrandpaEquivocation.java | 23 +++++ .../vote/GrandpaEquivocationScaleWriter.java | 36 ++++++++ .../java/com/limechain/runtime/Runtime.java | 9 +- .../limechain/runtime/RuntimeEndpoint.java | 2 + .../com/limechain/runtime/RuntimeImpl.java | 32 ++++++- 7 files changed, 168 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocation.java create mode 100644 src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocationScaleWriter.java diff --git a/src/main/java/com/limechain/babe/BlockProductionVerifier.java b/src/main/java/com/limechain/babe/BlockProductionVerifier.java index f48e6640a..739fb505f 100644 --- a/src/main/java/com/limechain/babe/BlockProductionVerifier.java +++ b/src/main/java/com/limechain/babe/BlockProductionVerifier.java @@ -201,10 +201,10 @@ private boolean isBlockEquivocationExist(byte[] authorityPublicKey, blockEquivocationProof.setFirstBlockHeader(firstBlockHeader); blockEquivocationProof.setSecondBlockHeader(blockHeader); - Optional opaqueKeyOwnershipProof = runtime.generateKeyOwnershipProof( + Optional 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))); diff --git a/src/main/java/com/limechain/network/protocol/grandpa/GrandpaMessageHandler.java b/src/main/java/com/limechain/network/protocol/grandpa/GrandpaMessageHandler.java index c314da04d..b381215a9 100644 --- a/src/main/java/com/limechain/network/protocol/grandpa/GrandpaMessageHandler.java +++ b/src/main/java/com/limechain/network/protocol/grandpa/GrandpaMessageHandler.java @@ -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; @@ -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; @@ -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; @@ -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 @@ -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); @@ -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 votes = PRE_COMMIT.equals(subRound) ? round.getPreCommits() : round.getPreVotes(); + Map> 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 = 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 @@ -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(); diff --git a/src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocation.java b/src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocation.java new file mode 100644 index 000000000..89dd03b74 --- /dev/null +++ b/src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocation.java @@ -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; +} diff --git a/src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocationScaleWriter.java b/src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocationScaleWriter.java new file mode 100644 index 000000000..d6d5489e7 --- /dev/null +++ b/src/main/java/com/limechain/network/protocol/grandpa/messages/vote/GrandpaEquivocationScaleWriter.java @@ -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 { + + 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()); + } +} diff --git a/src/main/java/com/limechain/runtime/Runtime.java b/src/main/java/com/limechain/runtime/Runtime.java index 6c9cc0f69..9b8e97917 100644 --- a/src/main/java/com/limechain/runtime/Runtime.java +++ b/src/main/java/com/limechain/runtime/Runtime.java @@ -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; @@ -23,9 +24,13 @@ public interface Runtime { BabeApiConfiguration getBabeApiConfiguration(); - Optional generateKeyOwnershipProof(BigInteger slotNumber, byte[] authorityPublicKey); + Optional generateBabeKeyOwnershipProof(BigInteger slotNumber, byte[] authorityPublicKey); - void submitReportEquivocationUnsignedExtrinsic(BlockEquivocationProof blockEquivocationProof, byte[] keyOwnershipProof); + void submitReportBabeEquivocationUnsignedExtrinsic(BlockEquivocationProof blockEquivocationProof, byte[] keyOwnershipProof); + + Optional generateGrandpaKeyOwnershipProof(BigInteger authoritySetId, byte[] authorityPublicKey); + + void submitReportGrandpaEquivocationUnsignedExtrinsic(GrandpaEquivocation grandpaEquivocation, byte[] keyOwnershipProof); List decodeSessionKeys(String sessionKeys); diff --git a/src/main/java/com/limechain/runtime/RuntimeEndpoint.java b/src/main/java/com/limechain/runtime/RuntimeEndpoint.java index c29ff48a9..97c88284d 100644 --- a/src/main/java/com/limechain/runtime/RuntimeEndpoint.java +++ b/src/main/java/com/limechain/runtime/RuntimeEndpoint.java @@ -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; diff --git a/src/main/java/com/limechain/runtime/RuntimeImpl.java b/src/main/java/com/limechain/runtime/RuntimeImpl.java index 3ce90076c..3ffd9d7c4 100644 --- a/src/main/java/com/limechain/runtime/RuntimeImpl.java +++ b/src/main/java/com/limechain/runtime/RuntimeImpl.java @@ -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; @@ -70,8 +72,8 @@ public BabeApiConfiguration getBabeApiConfiguration() { } @Override - public Optional generateKeyOwnershipProof(BigInteger slotNumber, - byte[] authorityPublicKey) { + public Optional 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); @@ -79,8 +81,8 @@ public Optional generateKeyOwnershipProof(BigInteger sl } @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); @@ -91,6 +93,28 @@ public void submitReportEquivocationUnsignedExtrinsic(BlockEquivocationProof blo } } + @Override + public Optional 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 decodeSessionKeys(String sessionKeys) { byte[] encodedRequest = ScaleUtils.Encode.encode(