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 <wgr523@gmail.com>
This commit is contained in:
Jerome 2022-08-13 14:20:56 +08:00 committed by GitHub
parent 22479f11ff
commit c710bd98a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 329 additions and 1 deletions

View file

@ -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
}

View file

@ -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))

View file

@ -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
}

View file

@ -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()
}
}
}

View file

@ -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())

View file

@ -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