go-ethereum/consensus/XDPoS/engines/engine_v2/forensics.go
2025-12-08 12:57:14 +05:30

450 lines
19 KiB
Go

package engine_v2
import (
"encoding/json"
"errors"
"fmt"
"math/big"
"strings"
"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/consensus"
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
"github.com/XinFinOrg/XDPoSChain/core/types"
"github.com/XinFinOrg/XDPoSChain/crypto"
"github.com/XinFinOrg/XDPoSChain/event"
"github.com/XinFinOrg/XDPoSChain/log"
)
const (
NUM_OF_FORENSICS_QC = 3
)
// Forensics instance. Placeholder for future properties to be added
type Forensics struct {
HighestCommittedQCs []types.QuorumCert
forensicsFeed event.Feed
scope event.SubscriptionScope
}
// Initiate a forensics process
func NewForensics() *Forensics {
return &Forensics{}
}
// SubscribeForensicsEvent registers a subscription of ForensicsEvent and
// starts sending event to the given channel.
func (f *Forensics) SubscribeForensicsEvent(ch chan<- types.ForensicsEvent) event.Subscription {
return f.scope.Track(f.forensicsFeed.Subscribe(ch))
}
func (f *Forensics) ForensicsMonitoring(chain consensus.ChainReader, engine *XDPoS_v2, headerQcToBeCommitted []types.Header, incomingQC types.QuorumCert) error {
f.ProcessForensics(chain, engine, incomingQC)
return f.SetCommittedQCs(headerQcToBeCommitted, incomingQC)
}
// Set the forensics committed QCs list. The order is from grandparent to current header. i.e it shall follow the QC in its header as follow [hcqc1, hcqc2, hcqc3]
func (f *Forensics) SetCommittedQCs(headers []types.Header, incomingQC types.QuorumCert) error {
// highestCommitQCs is an array, assign the parentBlockQc and its child as well as its grandchild QC into this array for forensics purposes.
if len(headers) != NUM_OF_FORENSICS_QC-1 {
log.Error("[SetCommittedQcs] Received input length not equal to 2", len(headers))
return errors.New("received headers length not equal to 2 ")
}
var committedQCs []types.QuorumCert
for i, h := range headers {
var decodedExtraField types.ExtraFields_v2
// Decode the qc1 and qc2
err := utils.DecodeBytesExtraFields(h.Extra, &decodedExtraField)
if err != nil {
log.Error("[SetCommittedQCs] Fail to decode extra when committing QC to forensics", "err", err, "index", i)
return err
}
if i != 0 {
if decodedExtraField.QuorumCert.ProposedBlockInfo.Hash != headers[i-1].Hash() {
log.Error("[SetCommittedQCs] Headers shall be on the same chain and in the right order", "parentHash", h.ParentHash.Hex(), "headers[i-1].Hash()", headers[i-1].Hash().Hex())
return errors.New("headers shall be on the same chain and in the right order")
} else if i == len(headers)-1 { // The last header shall be pointed by the incoming QC
if incomingQC.ProposedBlockInfo.Hash != h.Hash() {
log.Error("[SetCommittedQCs] incomingQc is not pointing at the last header received", "hash", h.Hash().Hex(), "incomingQC.ProposedBlockInfo.Hash", incomingQC.ProposedBlockInfo.Hash.Hex())
return errors.New("incomingQc is not pointing at the last header received")
}
}
}
committedQCs = append(committedQCs, *decodedExtraField.QuorumCert)
}
f.HighestCommittedQCs = append(committedQCs, incomingQC)
return nil
}
func (f *Forensics) ProcessForensics(chain consensus.ChainReader, engine *XDPoS_v2, incomingQC types.QuorumCert) error {
return nil
}
/*
Entry point for processing forensics.
Triggered once processQC is successfully.
Forensics runs in a separate go routine as its no system critical
Link to the flow diagram: https://hashlabs.atlassian.net/wiki/spaces/HASHLABS/pages/97878029/Forensics+Diagram+flow
func (f *Forensics) ProcessForensics(chain consensus.ChainReader, engine *XDPoS_v2, incomingQC types.QuorumCert) error {
log.Debug("Received a QC in forensics", "QC", incomingQC)
// Clone the values to a temporary variable
highestCommittedQCs := f.HighestCommittedQCs
if len(highestCommittedQCs) != NUM_OF_FORENSICS_QC {
log.Error("[ProcessForensics] HighestCommittedQCs value not set", "incomingQcProposedBlockHash", incomingQC.ProposedBlockInfo.Hash, "incomingQcProposedBlockNumber", incomingQC.ProposedBlockInfo.Number.Uint64(), "incomingQcProposedBlockRound", incomingQC.ProposedBlockInfo.Round)
return errors.New("HighestCommittedQCs value not set")
}
// Find the QC1 and QC2. We only care 2 parents in front of the incomingQC. The returned value contains QC1, QC2 and QC3(the incomingQC)
incomingQuorunCerts, err := f.findAncestorQCs(chain, incomingQC, 2)
if err != nil {
return err
}
isOnTheChain, err := f.checkQCsOnTheSameChain(chain, highestCommittedQCs, incomingQuorunCerts)
if err != nil {
return err
}
if isOnTheChain {
// Passed the checking, nothing suspicious.
log.Debug("[ProcessForensics] Passed forensics checking, nothing suspicious need to be reported", "incomingQcProposedBlockHash", incomingQC.ProposedBlockInfo.Hash, "incomingQcProposedBlockNumber", incomingQC.ProposedBlockInfo.Number.Uint64(), "incomingQcProposedBlockRound", incomingQC.ProposedBlockInfo.Round)
return nil
}
// Trigger the safety Alarm if failed
// First, find the QC in the two sets that have the same round
foundSameRoundQC, sameRoundHCQC, sameRoundQC := f.findQCsInSameRound(highestCommittedQCs, incomingQuorunCerts)
if foundSameRoundQC {
f.SendForensicProof(chain, engine, sameRoundHCQC, sameRoundQC)
} else {
// Not found, need a more complex approach to find the two QC
ancestorQC, lowerRoundQCs, _, err := f.findAncestorQcThroughRound(chain, highestCommittedQCs, incomingQuorunCerts)
if err != nil {
log.Error("[ProcessForensics] Error while trying to find ancestor QC through round number", "err", err)
}
f.SendForensicProof(chain, engine, ancestorQC, lowerRoundQCs[NUM_OF_FORENSICS_QC-1])
}
return nil
}
*/
// Last step of forensics which sends out detailed proof to report service.
func (f *Forensics) SendForensicProof(chain consensus.ChainReader, engine *XDPoS_v2, firstQc types.QuorumCert, secondQc types.QuorumCert) error {
// Re-order the QC by its round number to make the function cleaner.
lowerRoundQC := firstQc
higherRoundQC := secondQc
if secondQc.ProposedBlockInfo.Round < firstQc.ProposedBlockInfo.Round {
lowerRoundQC = secondQc
higherRoundQC = firstQc
}
// Find common ancestor block
ancestorHash, ancestorToLowerRoundPath, ancestorToHigherRoundPath, err := f.FindAncestorBlockHash(chain, lowerRoundQC.ProposedBlockInfo, higherRoundQC.ProposedBlockInfo)
if err != nil {
log.Error("[SendForensicProof] Error while trying to find ancestor block hash", "err", err)
return err
}
// Check if two QCs are across epoch, this is used as a indicator for the "prone to attack" scenario
lowerRoundQcEpochSwitchInfo, err := engine.getEpochSwitchInfo(chain, nil, lowerRoundQC.ProposedBlockInfo.Hash)
if err != nil {
log.Error("[SendForensicProof] Errir while trying to find lowerRoundQcEpochSwitchInfo", "lowerRoundQC.ProposedBlockInfo.Hash", lowerRoundQC.ProposedBlockInfo.Hash, "err", err)
return err
}
higherRoundQcEpochSwitchInfo, err := engine.getEpochSwitchInfo(chain, nil, higherRoundQC.ProposedBlockInfo.Hash)
if err != nil {
log.Error("[SendForensicProof] Errir while trying to find higherRoundQcEpochSwitchInfo", "higherRoundQC.ProposedBlockInfo.Hash", higherRoundQC.ProposedBlockInfo.Hash, "err", err)
return err
}
accrossEpoches := false
if lowerRoundQcEpochSwitchInfo.EpochSwitchBlockInfo.Hash != higherRoundQcEpochSwitchInfo.EpochSwitchBlockInfo.Hash {
accrossEpoches = true
}
ancestorBlock := chain.GetHeaderByHash(ancestorHash)
if ancestorBlock == nil {
log.Error("[SendForensicProof] Unable to find the ancestor block by its hash", "Hash", ancestorHash)
return errors.New("can't find ancestor block via hash")
}
content, err := json.Marshal(&types.ForensicsContent{
DivergingBlockHash: ancestorHash.Hex(),
AcrossEpoch: accrossEpoches,
DivergingBlockNumber: ancestorBlock.Number.Uint64(),
SmallerRoundInfo: &types.ForensicsInfo{
HashPath: ancestorToLowerRoundPath,
QuorumCert: lowerRoundQC,
SignerAddresses: f.getQcSignerAddresses(lowerRoundQC),
},
LargerRoundInfo: &types.ForensicsInfo{
HashPath: ancestorToHigherRoundPath,
QuorumCert: higherRoundQC,
SignerAddresses: f.getQcSignerAddresses(higherRoundQC),
},
})
if err != nil {
log.Error("[SendForensicProof] fail to json stringify forensics content", "err", err)
return err
}
forensicsProof := &types.ForensicProof{
Id: generateForensicsId(ancestorHash.Hex(), &lowerRoundQC, &higherRoundQC),
ForensicsType: "QC",
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
}
// Given the two QCs set, find if there are any QC that have the same round
func (f *Forensics) findQCsInSameRound(quorumCerts1 []types.QuorumCert, quorumCerts2 []types.QuorumCert) (bool, types.QuorumCert, types.QuorumCert) {
for _, quorumCert1 := range quorumCerts1 {
for _, quorumCert2 := range quorumCerts2 {
if quorumCert1.ProposedBlockInfo.Round == quorumCert2.ProposedBlockInfo.Round {
return true, quorumCert1, quorumCert2
}
}
}
return false, types.QuorumCert{}, types.QuorumCert{}
}
// Find the signer list from QC signatures
func (f *Forensics) getQcSignerAddresses(quorumCert types.QuorumCert) []string {
signerList := make([]string, 0, len(quorumCert.Signatures))
// The QC signatures are signed by votes special struct VoteForSign
quorumCertSignedHash := types.VoteSigHash(&types.VoteForSign{
ProposedBlockInfo: quorumCert.ProposedBlockInfo,
GapNumber: quorumCert.GapNumber,
})
for _, signature := range quorumCert.Signatures {
var signerAddress common.Address
pubkey, err := crypto.Ecrecover(quorumCertSignedHash.Bytes(), signature)
if err != nil {
log.Error("[getQcSignerAddresses] Fail to Ecrecover signer from the quorumCertSignedHash", "quorumCert.GapNumber", quorumCert.GapNumber, "quorumCert.ProposedBlockInfo", quorumCert.ProposedBlockInfo)
}
copy(signerAddress[:], crypto.Keccak256(pubkey[1:])[12:])
signerList = append(signerList, signerAddress.Hex())
}
return signerList
}
func (f *Forensics) FindAncestorBlockHash(chain consensus.ChainReader, firstBlockInfo *types.BlockInfo, secondBlockInfo *types.BlockInfo) (common.Hash, []string, []string, error) {
// Re-arrange by block number
lowerBlockNumHash := firstBlockInfo.Hash
higherBlockNumberHash := secondBlockInfo.Hash
var lowerBlockNumToAncestorHashPath []string
var higherBlockToAncestorNumHashPath []string
orderSwapped := false
blockNumberDifference := big.NewInt(0).Sub(secondBlockInfo.Number, firstBlockInfo.Number).Int64()
if blockNumberDifference < 0 {
lowerBlockNumHash = secondBlockInfo.Hash
higherBlockNumberHash = firstBlockInfo.Hash
blockNumberDifference = -blockNumberDifference // and make it positive
orderSwapped = true
}
lowerBlockNumToAncestorHashPath = append(lowerBlockNumToAncestorHashPath, lowerBlockNumHash.Hex())
higherBlockToAncestorNumHashPath = append(higherBlockToAncestorNumHashPath, higherBlockNumberHash.Hex())
// First, make their block number the same to start with
for i := 0; i < int(blockNumberDifference); i++ {
ph := chain.GetHeaderByHash(higherBlockNumberHash)
if ph == nil {
return common.Hash{}, lowerBlockNumToAncestorHashPath, higherBlockToAncestorNumHashPath, fmt.Errorf("unable to find parent block of hash %v", higherBlockNumberHash)
}
higherBlockNumberHash = ph.ParentHash
higherBlockToAncestorNumHashPath = append(higherBlockToAncestorNumHashPath, ph.ParentHash.Hex())
}
// Now, they are on the same starting line, we try find the common ancestor
for lowerBlockNumHash != higherBlockNumberHash {
lowerBlockNumHash = chain.GetHeaderByHash(lowerBlockNumHash).ParentHash
higherBlockNumberHash = chain.GetHeaderByHash(higherBlockNumberHash).ParentHash
// Append the path
lowerBlockNumToAncestorHashPath = append(lowerBlockNumToAncestorHashPath, lowerBlockNumHash.Hex())
higherBlockToAncestorNumHashPath = append(higherBlockToAncestorNumHashPath, higherBlockNumberHash.Hex())
}
// Reverse the list order as it's from ancestor to X block path.
ancestorToLowerBlockNumHashPath := reverse(lowerBlockNumToAncestorHashPath)
ancestorToHigherBlockNumHashPath := reverse(higherBlockToAncestorNumHashPath)
// Swap back the order. We must return in the order that matches what we acceptted in the parameter of firstBlock & secondBlock
if orderSwapped {
return lowerBlockNumHash, ancestorToHigherBlockNumHashPath, ancestorToLowerBlockNumHashPath, nil
}
return lowerBlockNumHash, ancestorToLowerBlockNumHashPath, ancestorToHigherBlockNumHashPath, nil
}
func generateForensicsId(divergingHash string, qc1 *types.QuorumCert, qc2 *types.QuorumCert) string {
keysList := []string{divergingHash, qc1.ProposedBlockInfo.Hash.Hex(), qc2.ProposedBlockInfo.Hash.Hex()}
return strings.Join(keysList[:], ":")
}
func reverse(ss []string) []string {
last := len(ss) - 1
for i := 0; i < len(ss)/2; i++ {
ss[i], ss[last-i] = ss[last-i], ss[i]
}
return ss
}
func generateVoteEquivocationId(signer common.Address, round1, round2 types.Round) string {
return fmt.Sprintf("%x:%d:%d", signer, round1, round2)
}
func (f *Forensics) ProcessVoteEquivocation(chain consensus.ChainReader, engine *XDPoS_v2, incomingVote *types.Vote) error {
return nil
}
/*
Entry point for processing vote equivocation.
Triggered once handle vote is successfully.
Forensics runs in a separate 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 errors.New("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 suspicious.
log.Debug("[ProcessVoteEquivocation] Passed forensics checking, nothing suspicious 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) DetectEquivocationInVotePool(vote *types.Vote, votePool *utils.Pool) {
return
}
/*
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
}