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 }