mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-19 21:31:37 +00:00
* 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:
parent
22479f11ff
commit
c710bd98a5
6 changed files with 329 additions and 1 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue