From c710bd98a5a2efbeaa9048dbca69498218f5cf22 Mon Sep 17 00:00:00 2001 From: Jerome Date: Sat, 13 Aug 2022 14:20:56 +0800 Subject: [PATCH] Xin-200 Vote Equivocation (#111) (#172) * Xin-200 Vote Equivocation (#111) * add vote same round detection add test for vote same round detect finish process equivocate (not finish report) * finish vote equivocation report, refactor code (vote -> forensics) * finish process equivocate and report, and test * add return err Co-authored-by: wgr523 --- .../XDPoS/engines/engine_v2/forensics.go | 180 ++++++++++++++++++ consensus/XDPoS/engines/engine_v2/vote.go | 2 + consensus/XDPoS/utils/pool.go | 17 ++ .../tests/engine_v2_tests/forensics_test.go | 121 ++++++++++++ consensus/tests/engine_v2_tests/vote_test.go | 2 +- core/types/forensics.go | 8 + 6 files changed, 329 insertions(+), 1 deletion(-) diff --git a/consensus/XDPoS/engines/engine_v2/forensics.go b/consensus/XDPoS/engines/engine_v2/forensics.go index 613240ce2f..bf9f0ddfb2 100644 --- a/consensus/XDPoS/engines/engine_v2/forensics.go +++ b/consensus/XDPoS/engines/engine_v2/forensics.go @@ -5,6 +5,7 @@ import ( "fmt" "math/big" "reflect" + "strconv" "strings" "github.com/XinFinOrg/XDPoSChain/common" @@ -380,3 +381,182 @@ func reverse(ss []string) []string { } return ss } + +func generateVoteEquivocationId(signer common.Address, round1, round2 types.Round) string { + return fmt.Sprintf("%x:%d:%d", signer, round1, round2) +} + +/* + Entry point for processing vote equivocation. + Triggered once handle vote is successfully. + Forensics runs in a seperate go routine as its no system critical + Link to the flow diagram: https://hashlabs.atlassian.net/wiki/spaces/HASHLABS/pages/99516417/Vote+Equivocation+detection+specification +*/ +func (f *Forensics) ProcessVoteEquivocation(chain consensus.ChainReader, engine *XDPoS_v2, incomingVote *types.Vote) error { + log.Debug("Received a vote in forensics", "vote", incomingVote) + // Clone the values to a temporary variable + highestCommittedQCs := f.HighestCommittedQCs + if len(highestCommittedQCs) != NUM_OF_FORENSICS_QC { + log.Error("[ProcessVoteEquivocation] HighestCommittedQCs value not set", "incomingVoteProposedBlockHash", incomingVote.ProposedBlockInfo.Hash, "incomingVoteProposedBlockNumber", incomingVote.ProposedBlockInfo.Number.Uint64(), "incomingVoteProposedBlockRound", incomingVote.ProposedBlockInfo.Round) + return fmt.Errorf("HighestCommittedQCs value not set") + } + if incomingVote.ProposedBlockInfo.Round < highestCommittedQCs[NUM_OF_FORENSICS_QC-1].ProposedBlockInfo.Round { + log.Debug("Received a too old vote in forensics", "vote", incomingVote) + return nil + } + // is vote extending committed block + isOnTheChain, err := f.isExtendingFromAncestor(chain, incomingVote.ProposedBlockInfo, highestCommittedQCs[0].ProposedBlockInfo) + if err != nil { + return err + } + if isOnTheChain { + // Passed the checking, nothing suspecious. + log.Debug("[ProcessVoteEquivocation] Passed forensics checking, nothing suspecious need to be reported", "incomingVoteProposedBlockHash", incomingVote.ProposedBlockInfo.Hash, "incomingVoteProposedBlockNumber", incomingVote.ProposedBlockInfo.Number.Uint64(), "incomingVoteProposedBlockRound", incomingVote.ProposedBlockInfo.Round) + return nil + } + // Trigger the safety Alarm if failed + isVoteBlamed, parentQC, err := f.isVoteBlamed(chain, highestCommittedQCs, incomingVote) + if err != nil { + log.Error("[ProcessVoteEquivocation] Error while trying to call isVoteBlamed", "error", err) + return err + } + if isVoteBlamed { + signer, err := GetVoteSignerAddresses(incomingVote) + if err != nil { + log.Error("[ProcessVoteEquivocation] GetVoteSignerAddresses", "error", err) + } + qc := highestCommittedQCs[NUM_OF_FORENSICS_QC-1] + for _, signature := range qc.Signatures { + voteFromQC := &types.Vote{ProposedBlockInfo: qc.ProposedBlockInfo, Signature: signature, GapNumber: qc.GapNumber} + signerFromQC, err := GetVoteSignerAddresses(voteFromQC) + if err != nil { + log.Error("[ProcessVoteEquivocation] GetVoteSignerAddresses", "error", err) + return err + } + if signerFromQC == signer { + f.SendVoteEquivocationProof(incomingVote, voteFromQC, signer) + break + } + } + // if no same-signer vote, nothing to report + } else { + // use the parent QC to do forensics + f.ProcessForensics(chain, engine, *parentQC) + } + + return nil +} + +func (f *Forensics) isExtendingFromAncestor(blockChainReader consensus.ChainReader, currentBlock *types.BlockInfo, ancestorBlock *types.BlockInfo) (bool, error) { + blockNumDiff := int(big.NewInt(0).Sub(currentBlock.Number, ancestorBlock.Number).Int64()) + + nextBlockHash := currentBlock.Hash + for i := 0; i < blockNumDiff; i++ { + parentBlock := blockChainReader.GetHeaderByHash(nextBlockHash) + if parentBlock == nil { + return false, fmt.Errorf("Could not find its parent block when checking whether currentBlock %v with hash %v is extending from the ancestorBlock %v", currentBlock.Number, currentBlock.Hash, ancestorBlock.Number) + } else { + nextBlockHash = parentBlock.ParentHash + } + log.Debug("[isExtendingFromAncestor] Found parent block", "CurrentBlockHash", currentBlock.Hash, "ParentHash", nextBlockHash) + } + + if nextBlockHash == ancestorBlock.Hash { + return true, nil + } + return false, nil +} + +func (f *Forensics) isVoteBlamed(chain consensus.ChainReader, highestCommittedQCs []types.QuorumCert, incomingVote *types.Vote) (bool, *types.QuorumCert, error) { + proposedBlock := chain.GetHeaderByHash(incomingVote.ProposedBlockInfo.Hash) + var decodedExtraField types.ExtraFields_v2 + err := utils.DecodeBytesExtraFields(proposedBlock.Extra, &decodedExtraField) + if err != nil { + log.Error("[findAncestorVoteThroughRound] Error while trying to decode extra field", "ProposedBlockInfo.Hash", incomingVote.ProposedBlockInfo.Hash) + return false, nil, err + } + // Found the parent QC, if its round < hcqc3's round, return true + if decodedExtraField.QuorumCert.ProposedBlockInfo.Round < highestCommittedQCs[NUM_OF_FORENSICS_QC-1].ProposedBlockInfo.Round { + return true, decodedExtraField.QuorumCert, nil + } + return false, decodedExtraField.QuorumCert, nil +} + +func (f *Forensics) DetectEquivocationInVotePool(vote *types.Vote, votePool *utils.Pool) { + poolKey := vote.PoolKey() + votePoolKeys := votePool.PoolObjKeysList() + signer, err := GetVoteSignerAddresses(vote) + if err != nil { + log.Error("[detectEquivocationInVotePool]", "err", err) + } + + for _, k := range votePoolKeys { + if k == poolKey { + continue + } + keyedRound, err := strconv.ParseInt(strings.Split(k, ":")[0], 10, 64) + if err != nil { + log.Error("[detectEquivocationInVotePool] Error while trying to get keyedRound inside pool", "Error", err) + continue + } + if types.Round(keyedRound) == vote.ProposedBlockInfo.Round { + votes := votePool.GetObjsByKey(k) + for _, v := range votes { + voteTransfered, ok := v.(*types.Vote) + if !ok { + log.Warn("[detectEquivocationInVotePool] obj type is not vote, potential a bug in votePool") + continue + } + signer2, err := GetVoteSignerAddresses(voteTransfered) + if err != nil { + log.Warn("[detectEquivocationInVotePool]", "err", err) + continue + } + if signer == signer2 { + f.SendVoteEquivocationProof(vote, voteTransfered, signer) + } + } + } + } +} + +func (f *Forensics) SendVoteEquivocationProof(vote1, vote2 *types.Vote, signer common.Address) error { + smallerRoundVote := vote1 + largerRoundVote := vote2 + if vote1.ProposedBlockInfo.Round > vote2.ProposedBlockInfo.Round { + smallerRoundVote = vote2 + largerRoundVote = vote1 + } + content, err := json.Marshal(&types.VoteEquivocationContent{ + SmallerRoundVote: smallerRoundVote, + LargerRoundVote: largerRoundVote, + Signer: signer, + }) + if err != nil { + log.Error("[SendVoteEquivocationProof] fail to json stringify forensics content", "err", err) + return err + } + forensicsProof := &types.ForensicProof{ + Id: generateVoteEquivocationId(signer, smallerRoundVote.ProposedBlockInfo.Round, largerRoundVote.ProposedBlockInfo.Round), + ForensicsType: "Vote", + Content: string(content), + } + log.Info("Forensics proof report generated, sending to the stats server", "forensicsProof", forensicsProof) + go f.forensicsFeed.Send(types.ForensicsEvent{ForensicsProof: forensicsProof}) + return nil +} + +func GetVoteSignerAddresses(vote *types.Vote) (common.Address, error) { + // The QC signatures are signed by votes special struct VoteForSign + signHash := types.VoteSigHash(&types.VoteForSign{ + ProposedBlockInfo: vote.ProposedBlockInfo, + GapNumber: vote.GapNumber, + }) + var signerAddress common.Address + pubkey, err := crypto.Ecrecover(signHash.Bytes(), vote.Signature) + if err != nil { + return signerAddress, fmt.Errorf("fail to Ecrecover signer from the vote: %v", vote) + } + copy(signerAddress[:], crypto.Keccak256(pubkey[1:])[12:]) + return signerAddress, nil +} diff --git a/consensus/XDPoS/engines/engine_v2/vote.go b/consensus/XDPoS/engines/engine_v2/vote.go index 37ea91a61a..60963ded22 100644 --- a/consensus/XDPoS/engines/engine_v2/vote.go +++ b/consensus/XDPoS/engines/engine_v2/vote.go @@ -67,6 +67,8 @@ func (x *XDPoS_v2) voteHandler(chain consensus.ChainReader, voteMsg *types.Vote) // Collect vote thresholdReached, numberOfVotesInPool, pooledVotes := x.votePool.Add(voteMsg) log.Debug("[voteHandler] collect votes", "number", numberOfVotesInPool) + go x.ForensicsProcessor.DetectEquivocationInVotePool(voteMsg, x.votePool) + go x.ForensicsProcessor.ProcessVoteEquivocation(chain, x, voteMsg) if thresholdReached { log.Info(fmt.Sprintf("[voteHandler] Vote pool threashold reached: %v, number of items in the pool: %v", thresholdReached, numberOfVotesInPool)) diff --git a/consensus/XDPoS/utils/pool.go b/consensus/XDPoS/utils/pool.go index b671e027fb..98eef6a4e7 100644 --- a/consensus/XDPoS/utils/pool.go +++ b/consensus/XDPoS/utils/pool.go @@ -91,3 +91,20 @@ func (p *Pool) SetThreshold(t int) { p.threshold = t } + +func (p *Pool) GetObjsByKey(poolKey string) []PoolObj { + p.lock.Lock() + defer p.lock.Unlock() + + objListKeyed, ok := p.objList[poolKey] + if !ok { + return []PoolObj{} + } + objList := make([]PoolObj, len(objListKeyed)) + cnt := 0 + for _, obj := range objListKeyed { + objList[cnt] = obj + cnt += 1 + } + return objList +} diff --git a/consensus/tests/engine_v2_tests/forensics_test.go b/consensus/tests/engine_v2_tests/forensics_test.go index 9b66133aa9..a55efaf0e8 100644 --- a/consensus/tests/engine_v2_tests/forensics_test.go +++ b/consensus/tests/engine_v2_tests/forensics_test.go @@ -311,3 +311,124 @@ func TestForensicsAcrossEpoch(t *testing.T) { } } } + +func TestVoteEquivocationSameRound(t *testing.T) { + var numOfForks = new(int) + *numOfForks = 1 + blockchain, _, currentBlock, signer, signFn, currentForkBlock := PrepareXDCTestBlockChainForV2Engine(t, 901, params.TestXDPoSMockChainConfig, &ForkedBlockOptions{numOfForkedBlocks: numOfForks}) + engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2 + // Set up forensics events trigger + forensics := blockchain.Engine().(*XDPoS.XDPoS).EngineV2.GetForensicsFaker() + forensicsEventCh := make(chan types.ForensicsEvent) + forensics.SubscribeForensicsEvent(forensicsEventCh) + // Set round to 5 + engineV2.SetNewRoundFaker(blockchain, types.Round(5), false) + + blockInfo := &types.BlockInfo{ + Hash: currentBlock.Hash(), + Round: types.Round(5), + Number: big.NewInt(901), + } + voteForSign := &types.VoteForSign{ + ProposedBlockInfo: blockInfo, + GapNumber: 450, + } + voteSigningHash := types.VoteSigHash(voteForSign) + signedHash, err := signFn(accounts.Account{Address: signer}, voteSigningHash.Bytes()) + assert.Nil(t, err) + voteMsg := &types.Vote{ + ProposedBlockInfo: blockInfo, + Signature: signedHash, + GapNumber: 450, + } + err = engineV2.VoteHandler(blockchain, voteMsg) + assert.Nil(t, err) + blockInfo = &types.BlockInfo{ + Hash: currentForkBlock.Hash(), + Round: types.Round(5), + Number: big.NewInt(901), + } + voteForSign = &types.VoteForSign{ + ProposedBlockInfo: blockInfo, + GapNumber: 450, + } + voteSigningHash = types.VoteSigHash(voteForSign) + signedHash, err = signFn(accounts.Account{Address: signer}, voteSigningHash.Bytes()) + assert.Nil(t, err) + voteMsg = &types.Vote{ + ProposedBlockInfo: blockInfo, + Signature: signedHash, + GapNumber: 450, + } + err = engineV2.VoteHandler(blockchain, voteMsg) + assert.Nil(t, err) + for { + select { + case msg := <-forensicsEventCh: + assert.NotNil(t, msg.ForensicsProof) + assert.Equal(t, "Vote", msg.ForensicsProof.ForensicsType) + content := &types.VoteEquivocationContent{} + json.Unmarshal([]byte(msg.ForensicsProof.Content), &content) + assert.Equal(t, types.Round(5), content.SmallerRoundVote.ProposedBlockInfo.Round) + assert.Equal(t, types.Round(5), content.LargerRoundVote.ProposedBlockInfo.Round) + return + case <-time.After(5 * time.Second): + t.FailNow() + } + } +} + +func TestVoteEquivocationDifferentRound(t *testing.T) { + var numOfForks = new(int) + *numOfForks = 10 + var forkRoundDifference = new(int) + *forkRoundDifference = 1 + var forkedChainSignersKey []*ecdsa.PrivateKey + forkedChainSignersKey = append(forkedChainSignersKey, acc1Key) + blockchain, _, _, _, _, currentForkBlock := PrepareXDCTestBlockChainForV2Engine(t, 915, params.TestXDPoSMockChainConfig, &ForkedBlockOptions{numOfForkedBlocks: numOfForks, forkedRoundDifference: forkRoundDifference, signersKey: forkedChainSignersKey}) + forensics := blockchain.Engine().(*XDPoS.XDPoS).EngineV2.GetForensicsFaker() + + // Now, let's try set committed blocks, where the highestedCommitted blocks are 913, 914 and 915 + var headers []types.Header + var decodedBlock915ExtraField types.ExtraFields_v2 + err := utils.DecodeBytesExtraFields(blockchain.GetHeaderByNumber(915).Extra, &decodedBlock915ExtraField) + assert.Nil(t, err) + err = forensics.SetCommittedQCs(append(headers, *blockchain.GetHeaderByNumber(913), *blockchain.GetHeaderByNumber(914)), *decodedBlock915ExtraField.QuorumCert) + assert.Nil(t, err) + + // find fork block 913 + forkBlock913 := blockchain.GetBlockByHash(blockchain.GetBlockByHash(currentForkBlock.ParentHash()).ParentHash()) + var decodedExtraField types.ExtraFields_v2 + // Decode the QC from forking chain + err = utils.DecodeBytesExtraFields(forkBlock913.Header().Extra, &decodedExtraField) + assert.Nil(t, err) + + incomingQC := decodedExtraField.QuorumCert + // choose just one vote from it + voteForSign := &types.VoteForSign{ProposedBlockInfo: incomingQC.ProposedBlockInfo, GapNumber: incomingQC.GapNumber} + voteForSign.ProposedBlockInfo.Round = types.Round(16) + signature := SignHashByPK(acc1Key, types.VoteSigHash(voteForSign).Bytes()) + incomingVote := &types.Vote{ProposedBlockInfo: voteForSign.ProposedBlockInfo, Signature: signature, GapNumber: voteForSign.GapNumber} + // Set up forensics events trigger + forensicsEventCh := make(chan types.ForensicsEvent) + forensics.SubscribeForensicsEvent(forensicsEventCh) + + err = forensics.ProcessVoteEquivocation(blockchain, blockchain.Engine().(*XDPoS.XDPoS).EngineV2, incomingVote) + assert.Nil(t, err) + // Check SendForensicProof triggered + for { + select { + case msg := <-forensicsEventCh: + assert.NotNil(t, msg.ForensicsProof) + assert.Equal(t, "Vote", msg.ForensicsProof.ForensicsType) + content := &types.VoteEquivocationContent{} + json.Unmarshal([]byte(msg.ForensicsProof.Content), &content) + assert.Equal(t, types.Round(14), content.SmallerRoundVote.ProposedBlockInfo.Round) + assert.Equal(t, types.Round(16), content.LargerRoundVote.ProposedBlockInfo.Round) + assert.Equal(t, acc1Addr, content.Signer) + return + case <-time.After(5 * time.Second): + t.FailNow() + } + } +} diff --git a/consensus/tests/engine_v2_tests/vote_test.go b/consensus/tests/engine_v2_tests/vote_test.go index 77b43c3804..3a25034a8c 100644 --- a/consensus/tests/engine_v2_tests/vote_test.go +++ b/consensus/tests/engine_v2_tests/vote_test.go @@ -33,7 +33,7 @@ func TestVoteMessageHandlerSuccessfullyGeneratedAndProcessQCForFistV2Round(t *te } voteSigningHash := types.VoteSigHash(voteForSign) - // Set round to 5 + // Set round to 1 engineV2.SetNewRoundFaker(blockchain, types.Round(1), false) // Create two vote messages which will not reach vote pool threshold signedHash, err := signFn(accounts.Account{Address: signer}, voteSigningHash.Bytes()) diff --git a/core/types/forensics.go b/core/types/forensics.go index 6f6f0413e7..d20efa5d01 100644 --- a/core/types/forensics.go +++ b/core/types/forensics.go @@ -1,5 +1,7 @@ package types +import "github.com/XinFinOrg/XDPoSChain/common" + type ForensicsInfo struct { HashPath []string `json:"hashPath"` QuorumCert QuorumCert `json:"quorumCert"` @@ -14,6 +16,12 @@ type ForensicsContent struct { LargerRoundInfo *ForensicsInfo `json:"largerRoundInfo"` } +type VoteEquivocationContent struct { + SmallerRoundVote *Vote `json:"smallerRoundVote"` + LargerRoundVote *Vote `json:"largerRoundVote"` + Signer common.Address +} + type ForensicProof struct { Id string `json:"id"` ForensicsType string `json:"forensicsType"` // QC or VOTE