mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-15 17:30:44 +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"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/XinFinOrg/XDPoSChain/common"
|
"github.com/XinFinOrg/XDPoSChain/common"
|
||||||
|
|
@ -380,3 +381,182 @@ func reverse(ss []string) []string {
|
||||||
}
|
}
|
||||||
return ss
|
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
|
// Collect vote
|
||||||
thresholdReached, numberOfVotesInPool, pooledVotes := x.votePool.Add(voteMsg)
|
thresholdReached, numberOfVotesInPool, pooledVotes := x.votePool.Add(voteMsg)
|
||||||
log.Debug("[voteHandler] collect votes", "number", numberOfVotesInPool)
|
log.Debug("[voteHandler] collect votes", "number", numberOfVotesInPool)
|
||||||
|
go x.ForensicsProcessor.DetectEquivocationInVotePool(voteMsg, x.votePool)
|
||||||
|
go x.ForensicsProcessor.ProcessVoteEquivocation(chain, x, voteMsg)
|
||||||
if thresholdReached {
|
if thresholdReached {
|
||||||
log.Info(fmt.Sprintf("[voteHandler] Vote pool threashold reached: %v, number of items in the pool: %v", thresholdReached, numberOfVotesInPool))
|
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
|
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)
|
voteSigningHash := types.VoteSigHash(voteForSign)
|
||||||
|
|
||||||
// Set round to 5
|
// Set round to 1
|
||||||
engineV2.SetNewRoundFaker(blockchain, types.Round(1), false)
|
engineV2.SetNewRoundFaker(blockchain, types.Round(1), false)
|
||||||
// Create two vote messages which will not reach vote pool threshold
|
// Create two vote messages which will not reach vote pool threshold
|
||||||
signedHash, err := signFn(accounts.Account{Address: signer}, voteSigningHash.Bytes())
|
signedHash, err := signFn(accounts.Account{Address: signer}, voteSigningHash.Bytes())
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
|
import "github.com/XinFinOrg/XDPoSChain/common"
|
||||||
|
|
||||||
type ForensicsInfo struct {
|
type ForensicsInfo struct {
|
||||||
HashPath []string `json:"hashPath"`
|
HashPath []string `json:"hashPath"`
|
||||||
QuorumCert QuorumCert `json:"quorumCert"`
|
QuorumCert QuorumCert `json:"quorumCert"`
|
||||||
|
|
@ -14,6 +16,12 @@ type ForensicsContent struct {
|
||||||
LargerRoundInfo *ForensicsInfo `json:"largerRoundInfo"`
|
LargerRoundInfo *ForensicsInfo `json:"largerRoundInfo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VoteEquivocationContent struct {
|
||||||
|
SmallerRoundVote *Vote `json:"smallerRoundVote"`
|
||||||
|
LargerRoundVote *Vote `json:"largerRoundVote"`
|
||||||
|
Signer common.Address
|
||||||
|
}
|
||||||
|
|
||||||
type ForensicProof struct {
|
type ForensicProof struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
ForensicsType string `json:"forensicsType"` // QC or VOTE
|
ForensicsType string `json:"forensicsType"` // QC or VOTE
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue