Skip to content

Commit ba144d8

Browse files
authored
process forensics (ethereum#84)
* process forensics * Found common signers at same round for forensics * find attackers * add test for forensics * run setCommittedQCs after processForensics
1 parent 5a15c45 commit ba144d8

17 files changed

+638
-122
lines changed

consensus/XDPoS/engines/engine_v2/engine.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,7 @@ func (x *XDPoS_v2) commitBlocks(blockChainReader consensus.ChainReader, proposed
933933
// Perform forensics related operation
934934
var headerQcToBeCommitted []types.Header
935935
headerQcToBeCommitted = append(headerQcToBeCommitted, *parentBlock, *proposedBlockHeader)
936-
go x.forensics.SetCommittedQCs(headerQcToBeCommitted, *incomingQc)
936+
go x.forensics.ForensicsMonitoring(blockChainReader, headerQcToBeCommitted, *incomingQc)
937937
return true, nil
938938
}
939939
// Everything else, fail to commit

consensus/XDPoS/engines/engine_v2/forensics.go

Lines changed: 203 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,29 @@ package engine_v2
22

33
import (
44
"fmt"
5+
"math/big"
6+
"reflect"
57

68
"github.com/XinFinOrg/XDPoSChain/common"
79
"github.com/XinFinOrg/XDPoSChain/consensus"
810
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
911
"github.com/XinFinOrg/XDPoSChain/core/types"
12+
"github.com/XinFinOrg/XDPoSChain/crypto"
1013
"github.com/XinFinOrg/XDPoSChain/log"
1114
)
1215

1316
const (
14-
NUM_OF_FORENSICS_PARENTS = 2
17+
NUM_OF_FORENSICS_QC = 3
1518
)
1619

1720
type ForensicProof struct {
18-
QcWithSmallerRound utils.QuorumCert
19-
QcWithLargerRound utils.QuorumCert
20-
DivergingHash common.Hash
21-
HashesTillSmallerRoundQc []common.Hash
22-
HashesTillLargerRoundQc []common.Hash
23-
AcrossEpochs bool
24-
QcWithSmallerRoundAddresses []common.Address
25-
QcWithLargerRoundAddresses []common.Address
21+
QcWithSmallerRound utils.QuorumCert
22+
QcWithLargerRound utils.QuorumCert
23+
DivergingHash common.Hash
24+
HashesTillSmallerRoundQc []common.Hash
25+
HashesTillLargerRoundQc []common.Hash
26+
AcrossEpochs bool
27+
Attackers []common.Address
2628
}
2729

2830
// Forensics instance. Placeholder for future properties to be added
@@ -35,20 +37,15 @@ func NewForensics() *Forensics {
3537
return &Forensics{}
3638
}
3739

38-
/*
39-
Entry point for processing forensics.
40-
Triggered once processQC is successfully.
41-
Forensics runs in a seperate go routine as its no system critical
42-
Link to the flow diagram: https://hashlabs.atlassian.net/wiki/spaces/HASHLABS/pages/97878029/Forensics+Diagram+flow
43-
*/
44-
func (f *Forensics) ProcessForensics(chain consensus.ChainReader, incomingQC utils.QuorumCert) {
45-
log.Info("Received a QC in forensics", "QC", incomingQC)
40+
func (f *Forensics) ForensicsMonitoring(chain consensus.ChainReader, headerQcToBeCommitted []types.Header, incomingQC utils.QuorumCert) error {
41+
f.ProcessForensics(chain, incomingQC)
42+
return f.SetCommittedQCs(headerQcToBeCommitted, incomingQC)
4643
}
4744

4845
// 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]
4946
func (f *Forensics) SetCommittedQCs(headers []types.Header, incomingQC utils.QuorumCert) error {
5047
// highestCommitQCs is an array, assign the parentBlockQc and its child as well as its grandchild QC into this array for forensics purposes.
51-
if len(headers) != NUM_OF_FORENSICS_PARENTS {
48+
if len(headers) != NUM_OF_FORENSICS_QC-1 {
5249
log.Error("[SetCommittedQcs] Received input length not equal to 2", len(headers))
5350
return fmt.Errorf("Received headers length not equal to 2 ")
5451
}
@@ -80,13 +77,197 @@ func (f *Forensics) SetCommittedQCs(headers []types.Header, incomingQC utils.Quo
8077
return nil
8178
}
8279

80+
/*
81+
Entry point for processing forensics.
82+
Triggered once processQC is successfully.
83+
Forensics runs in a seperate go routine as its no system critical
84+
Link to the flow diagram: https://hashlabs.atlassian.net/wiki/spaces/HASHLABS/pages/97878029/Forensics+Diagram+flow
85+
*/
86+
func (f *Forensics) ProcessForensics(chain consensus.ChainReader, incomingQC utils.QuorumCert) error {
87+
log.Debug("Received a QC in forensics", "QC", incomingQC)
88+
// Clone the values to a temporary variable
89+
highestCommittedQCs := f.HighestCommittedQCs
90+
if len(highestCommittedQCs) != NUM_OF_FORENSICS_QC {
91+
log.Error("[ProcessForensics] HighestCommittedQCs value not set", "incomingQcProposedBlockHash", incomingQC.ProposedBlockInfo.Hash, "incomingQcProposedBlockNumber", incomingQC.ProposedBlockInfo.Number.Uint64(), "incomingQcProposedBlockRound", incomingQC.ProposedBlockInfo.Round)
92+
return fmt.Errorf("HighestCommittedQCs value not set")
93+
}
94+
// 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)
95+
incomingQuorunCerts, err := f.findAncestorQCs(chain, incomingQC, 2)
96+
if err != nil {
97+
return err
98+
}
99+
isOnTheChain, err := f.checkQCsOnTheSameChain(chain, highestCommittedQCs, incomingQuorunCerts)
100+
if err != nil {
101+
return err
102+
}
103+
if isOnTheChain {
104+
// Passed the checking, nothing suspecious.
105+
log.Debug("[ProcessForensics] Passed forensics checking, nothing suspecious need to be reported", "incomingQcProposedBlockHash", incomingQC.ProposedBlockInfo.Hash, "incomingQcProposedBlockNumber", incomingQC.ProposedBlockInfo.Number.Uint64(), "incomingQcProposedBlockRound", incomingQC.ProposedBlockInfo.Round)
106+
return nil
107+
}
108+
// Trigger the safety Alarm if failed
109+
// First, find the QC in the two sets that have the same round
110+
foundSameRoundQC, sameRoundHCQC, sameRoundQC := f.findQCsInSameRound(highestCommittedQCs, incomingQuorunCerts)
111+
112+
if foundSameRoundQC {
113+
attackersAddress := f.findCommonSigners(sameRoundHCQC, sameRoundQC)
114+
f.SendForensicProof(attackersAddress, sameRoundHCQC, sameRoundQC)
115+
} else {
116+
// Not found, need a more complex approach to find the two QC
117+
ancestorQC, lowerRoundQCs, _, err := f.findAncestorQcThroughRound(chain, highestCommittedQCs, incomingQuorunCerts)
118+
if err != nil {
119+
log.Error("[ProcessForensics] Error while trying to find ancestor QC through round number", "Error", err)
120+
}
121+
// Find the common signers within ancestorQC and lowerRoundQCs[NUM_OF_FORENSICS_QC-1]
122+
attackersAddress := f.findCommonSigners(ancestorQC, lowerRoundQCs[NUM_OF_FORENSICS_QC-1])
123+
f.SendForensicProof(attackersAddress, ancestorQC, lowerRoundQCs[NUM_OF_FORENSICS_QC-1])
124+
}
125+
126+
return nil
127+
}
128+
83129
// Last step of forensics which sends out detailed proof to report service.
84-
func (f *Forensics) SendForensicProof() {
130+
func (f *Forensics) SendForensicProof(attackersAddress []common.Address, lqc utils.QuorumCert, hqc utils.QuorumCert) {
131+
132+
}
133+
134+
// Utils function to help find the n-th previous QC. It returns an array of QC in ascending order including the currentQc as the last item in the array
135+
func (f *Forensics) findAncestorQCs(chain consensus.ChainReader, currentQc utils.QuorumCert, distanceFromCurrrentQc int) ([]utils.QuorumCert, error) {
136+
var quorumCerts []utils.QuorumCert
137+
quorumCertificate := currentQc
138+
// Append the initial value
139+
quorumCerts = append(quorumCerts, quorumCertificate)
140+
// Append the parents
141+
for i := 0; i < distanceFromCurrrentQc; i++ {
142+
parentHash := quorumCertificate.ProposedBlockInfo.Hash
143+
parentHeader := chain.GetHeaderByHash(parentHash)
144+
if parentHeader == nil {
145+
log.Error("[findAncestorQCs] Forensics findAncestorQCs unable to find its parent block header", "BlockNum", parentHeader.Number.Int64(), "ParentHash", parentHash.Hex())
146+
return nil, fmt.Errorf("Unable to find parent block header in forensics")
147+
}
148+
var decodedExtraField utils.ExtraFields_v2
149+
err := utils.DecodeBytesExtraFields(parentHeader.Extra, &decodedExtraField)
150+
if err != nil {
151+
log.Error("[findAncestorQCs] Error while trying to decode from parent block extra", "BlockNum", parentHeader.Number.Int64(), "ParentHash", parentHash.Hex())
152+
}
153+
quorumCertificate = *decodedExtraField.QuorumCert
154+
quorumCerts = append(quorumCerts, quorumCertificate)
155+
}
156+
// The quorumCerts is in the reverse order, we need to flip it
157+
var quorumCertsInAscendingOrder []utils.QuorumCert
158+
for i := len(quorumCerts) - 1; i >= 0; i-- {
159+
quorumCertsInAscendingOrder = append(quorumCertsInAscendingOrder, quorumCerts[i])
160+
}
161+
return quorumCertsInAscendingOrder, nil
162+
}
163+
164+
// Check whether two provided QC set are on the same chain
165+
func (f *Forensics) checkQCsOnTheSameChain(chain consensus.ChainReader, highestCommittedQCs []utils.QuorumCert, incomingQCandItsParents []utils.QuorumCert) (bool, error) {
166+
// Re-order two sets of QCs by block Number
167+
lowerBlockNumQCs := highestCommittedQCs
168+
higherBlockNumQCs := incomingQCandItsParents
169+
if incomingQCandItsParents[0].ProposedBlockInfo.Number.Cmp(highestCommittedQCs[0].ProposedBlockInfo.Number) == -1 {
170+
lowerBlockNumQCs = incomingQCandItsParents
171+
higherBlockNumQCs = highestCommittedQCs
172+
}
173+
174+
proposedBlockInfo := higherBlockNumQCs[0].ProposedBlockInfo
175+
for i := 0; i < int((big.NewInt(0).Sub(higherBlockNumQCs[0].ProposedBlockInfo.Number, lowerBlockNumQCs[0].ProposedBlockInfo.Number)).Int64()); i++ {
176+
parentHeader := chain.GetHeaderByHash(proposedBlockInfo.Hash)
177+
var decodedExtraField utils.ExtraFields_v2
178+
err := utils.DecodeBytesExtraFields(parentHeader.Extra, &decodedExtraField)
179+
if err != nil {
180+
log.Error("[ProcessForensics] Fail to decode extra when checking the two QCs set on the same chain", "Error", err)
181+
return false, err
182+
}
183+
proposedBlockInfo = decodedExtraField.QuorumCert.ProposedBlockInfo
184+
}
185+
// Check the final proposed blockInfo is the same as what we have from lowerBlockNumQCs[0]
186+
if reflect.DeepEqual(proposedBlockInfo, lowerBlockNumQCs[0].ProposedBlockInfo) {
187+
return true, nil
188+
}
189+
190+
return false, nil
191+
}
192+
193+
// Given the two QCs set, find if there are any QC that have the same round
194+
func (f *Forensics) findQCsInSameRound(quorumCerts1 []utils.QuorumCert, quorumCerts2 []utils.QuorumCert) (bool, utils.QuorumCert, utils.QuorumCert) {
195+
for _, quorumCert1 := range quorumCerts1 {
196+
for _, quorumCert2 := range quorumCerts2 {
197+
if quorumCert1.ProposedBlockInfo.Round == quorumCert2.ProposedBlockInfo.Round {
198+
return true, quorumCert1, quorumCert2
199+
}
200+
}
201+
}
202+
return false, utils.QuorumCert{}, utils.QuorumCert{}
85203
}
86204

87-
// Find the blockInfo of the block -2 distance away from the QC. Note: We using block number which means not necessary on the same chain as QC received
88-
func (f *Forensics) findParentsQc(chain consensus.ChainReader, currentQc utils.QuorumCert, distanceFromCurrrentQc int64) {
205+
// Find the common address that have signed both QCs
206+
func (f *Forensics) findCommonSigners(quorumCert1 utils.QuorumCert, quorumCert2 utils.QuorumCert) []common.Address {
207+
var commonSigners []common.Address
208+
quorumCert1SignersMap := make(map[string]bool)
209+
// The QC signatures are signed by votes special struct VoteForSign
210+
quorumCert1SignedHash := utils.VoteSigHash(&utils.VoteForSign{
211+
ProposedBlockInfo: quorumCert1.ProposedBlockInfo,
212+
GapNumber: quorumCert1.GapNumber,
213+
})
214+
for _, signature := range quorumCert1.Signatures {
215+
var signerAddress common.Address
216+
pubkey, err := crypto.Ecrecover(quorumCert1SignedHash.Bytes(), signature)
217+
if err != nil {
218+
log.Error("[findCommonSigners] Fail to Ecrecover signer from the quorumCert1SignedHash", "quorumCert1.GapNumber", quorumCert1.GapNumber, "quorumCert1.ProposedBlockInfo", quorumCert1.ProposedBlockInfo)
219+
}
220+
221+
copy(signerAddress[:], crypto.Keccak256(pubkey[1:])[12:])
222+
quorumCert1SignersMap[signerAddress.Hex()] = true
223+
}
224+
// Now, Let's check if quorumCert2 have any signers that have in common with quorumCert1SignersMap(from quorumCert1)
225+
quorumCert2SignedHash := utils.VoteSigHash(&utils.VoteForSign{
226+
ProposedBlockInfo: quorumCert2.ProposedBlockInfo,
227+
GapNumber: quorumCert2.GapNumber,
228+
})
229+
for _, signature := range quorumCert2.Signatures {
230+
var signerAddress common.Address
231+
pubkey, err := crypto.Ecrecover(quorumCert2SignedHash.Bytes(), signature)
232+
if err != nil {
233+
log.Error("[findCommonSigners] Fail to Ecrecover signer from the quorumCert2SignedHash", "quorumCert2.GapNumber", quorumCert2.GapNumber, "quorumCert2.ProposedBlockInfo", quorumCert2.ProposedBlockInfo)
234+
}
235+
236+
copy(signerAddress[:], crypto.Keccak256(pubkey[1:])[12:])
237+
if quorumCert1SignersMap[signerAddress.Hex()] {
238+
commonSigners = append(commonSigners, signerAddress)
239+
}
240+
}
241+
return commonSigners
89242
}
90243

91-
func (f *Forensics) findCommonSigners(currentQc utils.QuorumCert, higherQc utils.QuorumCert) {
244+
// Check whether the given QCs are on the same chain as the stored committed QCs(f.HighestCommittedQCs) regardless their orders
245+
func (f *Forensics) findAncestorQcThroughRound(chain consensus.ChainReader, highestCommittedQCs []utils.QuorumCert, incomingQCandItsParents []utils.QuorumCert) (utils.QuorumCert, []utils.QuorumCert, []utils.QuorumCert, error) {
246+
/*
247+
Re-order two sets of QCs by Round number
248+
*/
249+
lowerRoundQCs := highestCommittedQCs
250+
higherRoundQCs := incomingQCandItsParents
251+
if incomingQCandItsParents[0].ProposedBlockInfo.Round < highestCommittedQCs[0].ProposedBlockInfo.Round {
252+
lowerRoundQCs = incomingQCandItsParents
253+
higherRoundQCs = highestCommittedQCs
254+
}
255+
256+
// Find the ancestorFromIncomingQC1 that matches round number < lowerRoundQCs3
257+
ancestorQC := higherRoundQCs[0]
258+
for ancestorQC.ProposedBlockInfo.Round >= lowerRoundQCs[NUM_OF_FORENSICS_QC-1].ProposedBlockInfo.Round {
259+
proposedBlock := chain.GetHeaderByHash(ancestorQC.ProposedBlockInfo.Hash)
260+
var decodedExtraField utils.ExtraFields_v2
261+
err := utils.DecodeBytesExtraFields(proposedBlock.Extra, &decodedExtraField)
262+
if err != nil {
263+
log.Error("[findAncestorQcThroughRound] Error while trying to decode extra field", "ProposedBlockInfo.Hash", ancestorQC.ProposedBlockInfo.Hash)
264+
return ancestorQC, lowerRoundQCs, higherRoundQCs, err
265+
}
266+
// Found the ancestor QC
267+
if decodedExtraField.QuorumCert.ProposedBlockInfo.Round < lowerRoundQCs[NUM_OF_FORENSICS_QC-1].ProposedBlockInfo.Round {
268+
return ancestorQC, lowerRoundQCs, higherRoundQCs, nil
269+
}
270+
ancestorQC = *decodedExtraField.QuorumCert
271+
}
272+
return ancestorQC, lowerRoundQCs, higherRoundQCs, fmt.Errorf("[findAncestorQcThroughRound] Could not find ancestor QC")
92273
}

0 commit comments

Comments
 (0)