Skip to content

Commit b54b2b8

Browse files
authored
fix(consensus): apply v2.7.0 hotfix to dev branch and revert optimization changes (#2357)
* Revert "fix(consensus): use signer pubkey to check for unique signatures and optimize performance, close XFN-03 (#1625)" This reverts commit 81416e0. * Revert "add syncinfo pool (#1236)" This reverts commit db9c3de. * consensus: use signer pubkey to check for unique signatures, close XFN-03 (#1643) * use signer pubkey to check for unique signatures * add error handling * add go routines to process Ecrecover concurrently add input validation and early return * optimize * Merge commit from fork (fix(consensus): signature dedup construction check) * add signature dedup construction check at QC and TC generation process * add tests * add checking of signature S value to prevent malleable signature (signature flipping) * add signature flip tests * fix test * fix test * refactor hasLowS check to aware of signer and improve logs * fix inaccurate pubkeys > addresses (fix): fix tests due to change in error logging (#2352) * keep logging improvement and dont revert config change * fix formatting * fix from copilot suggestions
1 parent dd07787 commit b54b2b8

17 files changed

Lines changed: 1036 additions & 509 deletions

File tree

consensus/XDPoS/api.go

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,7 @@ const (
119119
statusObservernode AccountRewardStatus = "ObserverNode"
120120
)
121121

122-
type MessageStatus map[string]map[string]interface{}
123-
124-
type SyncInfoTypes struct {
125-
Hash common.Hash `json:"hash"`
126-
QCSigners int `json:"qcSigners"`
127-
TCSigners int `json:"tcSigners"`
128-
}
129-
130-
type PoolStatus struct {
131-
Vote map[string]SignerTypes `json:"vote"`
132-
Timeout map[string]SignerTypes `json:"timeout"`
133-
SyncInfo map[string]SyncInfoTypes `json:"syncInfo"`
134-
}
122+
type MessageStatus map[string]map[string]SignerTypes
135123

136124
// GetSnapshot retrieves the state snapshot at a given block.
137125
func (api *API) GetSnapshot(number *rpc.BlockNumber) (*utils.PublicApiSnapshot, error) {
@@ -242,40 +230,18 @@ func (api *API) GetMasternodesByNumber(number *rpc.BlockNumber) MasternodesStatu
242230
}
243231

244232
// Get current vote pool and timeout pool content and missing messages
245-
func (api *API) GetLatestPoolStatus() PoolStatus {
233+
func (api *API) GetLatestPoolStatus() MessageStatus {
246234
header := api.chain.CurrentHeader()
247235
masternodes := api.XDPoS.EngineV2.GetMasternodes(api.chain, header)
248236

249237
receivedVotes := api.XDPoS.EngineV2.ReceivedVotes()
250238
receivedTimeouts := api.XDPoS.EngineV2.ReceivedTimeouts()
251-
receivedSyncInfo := api.XDPoS.EngineV2.ReceivedSyncInfo()
252-
253-
info := PoolStatus{}
254-
info.Vote = make(map[string]SignerTypes)
255-
info.Timeout = make(map[string]SignerTypes)
256-
info.SyncInfo = make(map[string]SyncInfoTypes)
257-
258-
calculateSigners(info.Vote, receivedVotes, masternodes)
259-
calculateSigners(info.Timeout, receivedTimeouts, masternodes)
239+
info := make(MessageStatus)
240+
info["vote"] = make(map[string]SignerTypes)
241+
info["timeout"] = make(map[string]SignerTypes)
260242

261-
for name, objs := range receivedSyncInfo {
262-
for _, obj := range objs {
263-
syncInfo := obj.(*types.SyncInfo)
264-
hash := syncInfo.Hash()
265-
key := name + ":" + hash.Hex()
266-
267-
qcSigners := len(syncInfo.HighestQuorumCert.Signatures)
268-
tcSigners := 0
269-
if syncInfo.HighestTimeoutCert != nil {
270-
tcSigners = len(syncInfo.HighestTimeoutCert.Signatures)
271-
}
272-
info.SyncInfo[key] = SyncInfoTypes{
273-
Hash: hash,
274-
QCSigners: qcSigners,
275-
TCSigners: tcSigners,
276-
}
277-
}
278-
}
243+
calculateSigners(info["vote"], receivedVotes, masternodes)
244+
calculateSigners(info["timeout"], receivedTimeouts, masternodes)
279245

280246
return info
281247
}

consensus/XDPoS/engines/engine_v2/engine.go

Lines changed: 193 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package engine_v2
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"errors"
78
"fmt"
89
"math/big"
910
"os"
1011
"path/filepath"
12+
"runtime"
1113
"strconv"
1214
"sync"
1315
"time"
@@ -27,6 +29,7 @@ import (
2729
"github.com/XinFinOrg/XDPoSChain/log"
2830
"github.com/XinFinOrg/XDPoSChain/params"
2931
"github.com/XinFinOrg/XDPoSChain/trie"
32+
"golang.org/x/sync/errgroup"
3033
)
3134

3235
type XDPoS_v2 struct {
@@ -60,7 +63,6 @@ type XDPoS_v2 struct {
6063

6164
timeoutPool *utils.Pool
6265
votePool *utils.Pool
63-
syncInfoPool *utils.Pool
6466
currentRound types.Round
6567
highestSelfMinedRound types.Round
6668
highestVotedRound types.Round
@@ -92,7 +94,6 @@ func New(chainConfig *params.ChainConfig, db ethdb.Database, minePeriodCh chan i
9294

9395
timeoutPool := utils.NewPool()
9496
votePool := utils.NewPool()
95-
syncInfoPool := utils.NewPool()
9697
engine := &XDPoS_v2{
9798
chainConfig: chainConfig,
9899

@@ -112,9 +113,8 @@ func New(chainConfig *params.ChainConfig, db ethdb.Database, minePeriodCh chan i
112113

113114
round2epochBlockInfo: lru.NewCache[types.Round, *types.BlockInfo](utils.InMemoryRound2Epochs),
114115

115-
timeoutPool: timeoutPool,
116-
votePool: votePool,
117-
syncInfoPool: syncInfoPool,
116+
timeoutPool: timeoutPool,
117+
votePool: votePool,
118118

119119
highestSelfMinedRound: types.Round(0),
120120

@@ -647,6 +647,147 @@ func (x *XDPoS_v2) VerifyHeaders(chain consensus.ChainReader, headers []*types.H
647647
}()
648648
}
649649

650+
/*
651+
SyncInfo workflow
652+
*/
653+
// Verify syncInfo and trigger process QC or TC if successful
654+
func (x *XDPoS_v2) VerifySyncInfoMessage(chain consensus.ChainReader, syncInfo *types.SyncInfo) (bool, error) {
655+
/*
656+
1. Check QC and TC against highest QC TC. Skip if none of them need to be updated
657+
2. Verify items including:
658+
- verifyQC
659+
- verifyTC
660+
3. Broadcast(Not part of consensus)
661+
*/
662+
663+
if (x.highestQuorumCert.ProposedBlockInfo.Round >= syncInfo.HighestQuorumCert.ProposedBlockInfo.Round) && (x.highestTimeoutCert.Round >= syncInfo.HighestTimeoutCert.Round) {
664+
log.Debug("[VerifySyncInfoMessage] Round from incoming syncInfo message is no longer qualified", "Highest QC Round", x.highestQuorumCert.ProposedBlockInfo.Round, "Incoming SyncInfo QC Round", syncInfo.HighestQuorumCert.ProposedBlockInfo.Round, "highestTimeoutCert Round", x.highestTimeoutCert.Round, "Incoming syncInfo TC Round", syncInfo.HighestTimeoutCert.Round)
665+
return false, nil
666+
}
667+
668+
err := x.verifyQC(chain, syncInfo.HighestQuorumCert, nil)
669+
if err != nil {
670+
log.Warn("[VerifySyncInfoMessage] SyncInfo message verification failed due to QC", "blockNum", syncInfo.HighestQuorumCert.ProposedBlockInfo.Number, "round", syncInfo.HighestQuorumCert.ProposedBlockInfo.Round, "error", err)
671+
return false, err
672+
}
673+
err = x.verifyTC(chain, syncInfo.HighestTimeoutCert)
674+
if err != nil {
675+
log.Warn("[VerifySyncInfoMessage] SyncInfo message verification failed due to TC", "gapNum", syncInfo.HighestTimeoutCert.GapNumber, "round", syncInfo.HighestTimeoutCert.Round, "error", err)
676+
return false, err
677+
}
678+
return true, nil
679+
}
680+
681+
func (x *XDPoS_v2) SyncInfoHandler(chain consensus.ChainReader, syncInfo *types.SyncInfo) error {
682+
x.lock.Lock()
683+
defer x.lock.Unlock()
684+
/*
685+
1. processQC
686+
2. processTC
687+
*/
688+
err := x.processQC(chain, syncInfo.HighestQuorumCert)
689+
if err != nil {
690+
return err
691+
}
692+
return x.processTC(chain, syncInfo.HighestTimeoutCert)
693+
}
694+
695+
/*
696+
Vote workflow
697+
*/
698+
func (x *XDPoS_v2) VerifyVoteMessage(chain consensus.ChainReader, vote *types.Vote) (bool, error) {
699+
/*
700+
1. Check vote round with current round for fast fail(disqualifed)
701+
2. Get masterNode list from snapshot by using vote.GapNumber
702+
3. Check signature:
703+
- Use ecRecover to get the public key
704+
- Use the above public key to find out the xdc address
705+
- Use the above xdc address to check against the master node list from step 1(For the running epoch)
706+
4. Broadcast(Not part of consensus)
707+
*/
708+
if vote.ProposedBlockInfo.Round < x.currentRound {
709+
log.Debug("[VerifyVoteMessage] Disqualified vote message as the proposed round does not match currentRound", "voteHash", vote.Hash(), "voteProposedBlockInfoRound", vote.ProposedBlockInfo.Round, "currentRound", x.currentRound)
710+
return false, nil
711+
}
712+
713+
snapshot, err := x.getSnapshot(chain, vote.GapNumber, true)
714+
if err != nil {
715+
log.Error("[VerifyVoteMessage] fail to get snapshot for a vote message", "blockNum", vote.ProposedBlockInfo.Number, "blockHash", vote.ProposedBlockInfo.Hash, "voteHash", vote.Hash(), "error", err.Error())
716+
return false, err
717+
}
718+
verified, signer, err := x.verifyMsgSignature(types.VoteSigHash(&types.VoteForSign{
719+
ProposedBlockInfo: vote.ProposedBlockInfo,
720+
GapNumber: vote.GapNumber,
721+
}), vote.Signature, snapshot.NextEpochCandidates)
722+
if err != nil {
723+
for i, mn := range snapshot.NextEpochCandidates {
724+
log.Warn("[VerifyVoteMessage] Master node list item", "index", i, "Master node", mn.Hex())
725+
}
726+
log.Warn("[VerifyVoteMessage] Error while verifying vote message", "votedBlockNum", vote.ProposedBlockInfo.Number.Uint64(), "votedBlockHash", vote.ProposedBlockInfo.Hash.Hex(), "voteHash", vote.Hash(), "error", err.Error())
727+
return false, err
728+
}
729+
vote.SetSigner(signer)
730+
731+
return verified, nil
732+
}
733+
734+
// Consensus entry point for processing vote message to produce QC
735+
func (x *XDPoS_v2) VoteHandler(chain consensus.ChainReader, voteMsg *types.Vote) error {
736+
x.lock.Lock()
737+
defer x.lock.Unlock()
738+
return x.voteHandler(chain, voteMsg)
739+
}
740+
741+
/*
742+
Timeout workflow
743+
*/
744+
// Verify timeout message type from peers in bft.go
745+
/*
746+
1. Get master node list by timeout msg round
747+
2. Check signature:
748+
- Use ecRecover to get the public key
749+
- Use the above public key to find out the xdc address
750+
- Use the above xdc address to check against the master node list from step 1(For the running epoch)
751+
3. Broadcast(Not part of consensus)
752+
*/
753+
func (x *XDPoS_v2) VerifyTimeoutMessage(chain consensus.ChainReader, timeoutMsg *types.Timeout) (bool, error) {
754+
if timeoutMsg.Round < x.currentRound {
755+
log.Debug("[VerifyTimeoutMessage] Disqualified timeout message as the proposed round does not match currentRound", "timeoutHash", timeoutMsg.Hash(), "timeoutRound", timeoutMsg.Round, "currentRound", x.currentRound)
756+
return false, nil
757+
}
758+
snap, err := x.getSnapshot(chain, timeoutMsg.GapNumber, true)
759+
if err != nil || snap == nil {
760+
log.Error("[VerifyTimeoutMessage] Fail to get snapshot when verifying timeout message!", "messageGapNumber", timeoutMsg.GapNumber, "err", err)
761+
return false, err
762+
}
763+
if len(snap.NextEpochCandidates) == 0 {
764+
log.Error("[VerifyTimeoutMessage] cannot find NextEpochCandidates from snapshot", "messageGapNumber", timeoutMsg.GapNumber)
765+
return false, errors.New("empty master node lists from snapshot")
766+
}
767+
768+
verified, signer, err := x.verifyMsgSignature(types.TimeoutSigHash(&types.TimeoutForSign{
769+
Round: timeoutMsg.Round,
770+
GapNumber: timeoutMsg.GapNumber,
771+
}), timeoutMsg.Signature, snap.NextEpochCandidates)
772+
773+
if err != nil {
774+
log.Warn("[VerifyTimeoutMessage] cannot verify timeout signature", "err", err)
775+
return false, err
776+
}
777+
778+
timeoutMsg.SetSigner(signer)
779+
return verified, nil
780+
}
781+
782+
/*
783+
Entry point for handling timeout message to process below:
784+
*/
785+
func (x *XDPoS_v2) TimeoutHandler(blockChainReader consensus.ChainReader, timeout *types.Timeout) error {
786+
x.lock.Lock()
787+
defer x.lock.Unlock()
788+
return x.timeoutHandler(blockChainReader, timeout)
789+
}
790+
650791
/*
651792
Proposed Block workflow
652793
*/
@@ -756,23 +897,59 @@ func (x *XDPoS_v2) verifyQC(blockChainReader consensus.ChainReader, quorumCert *
756897
ProposedBlockInfo: quorumCert.ProposedBlockInfo,
757898
GapNumber: quorumCert.GapNumber,
758899
})
759-
start := time.Now()
760-
numValidSignatures, err := x.countValidSignatures(signedVoteObj, quorumCert.Signatures, epochInfo.Masternodes)
761-
elapsed := time.Since(start)
762-
log.Debug("[verifyQC] time verify message signatures of qc", "elapsed", elapsed)
900+
901+
signatures, duplicates, err := RecoverUniqueSigners(signedVoteObj, quorumCert.Signatures)
763902
if err != nil {
764-
log.Error("[verifyQC] Error while verifying QC message signatures", "Error", err)
903+
log.Error("[verifyQC] Error while getting unique signatures from QC", "qcBlockNum", quorumCert.ProposedBlockInfo.Number, "qcRound", quorumCert.ProposedBlockInfo.Round, "qcBlockHash", quorumCert.ProposedBlockInfo.Hash, "qcSignLen", len(quorumCert.Signatures), "error", err)
765904
return err
766905
}
767906

907+
if len(duplicates) != 0 {
908+
for _, d := range duplicates {
909+
log.Warn("[verifyQC] duplicated signature in QC", "duplicate", common.Bytes2Hex(d))
910+
}
911+
}
912+
768913
qcRound := quorumCert.ProposedBlockInfo.Round
769914
certThreshold := x.config.V2.Config(uint64(qcRound)).CertThreshold
770-
if (qcRound > 0) && (float64(numValidSignatures) < float64(epochInfo.MasternodesLen)*certThreshold) {
915+
if (qcRound > 0) && (signatures == nil || float64(len(signatures)) < float64(epochInfo.MasternodesLen)*certThreshold) {
771916
//First V2 Block QC, QC Signatures is initial nil
772-
log.Warn("[verifyHeader] Invalid QC Signature is nil or less then config", "QCNumber", quorumCert.ProposedBlockInfo.Number, "numValidSignatures", numValidSignatures, "CertThreshold", float64(epochInfo.MasternodesLen)*certThreshold)
917+
log.Warn("[verifyHeader] Invalid QC Signature is nil or less then config", "QCNumber", quorumCert.ProposedBlockInfo.Number, "LenSignatures", len(signatures), "CertThreshold", float64(epochInfo.MasternodesLen)*certThreshold)
773918
return utils.ErrInvalidQCSignatures
774919
}
920+
start := time.Now()
921+
922+
eg, ctx := errgroup.WithContext(context.Background())
923+
eg.SetLimit(runtime.NumCPU())
924+
for _, sig := range signatures {
925+
eg.Go(func() error {
926+
select {
927+
case <-ctx.Done():
928+
return ctx.Err()
929+
default:
930+
verified, _, err := x.verifyMsgSignature(types.VoteSigHash(&types.VoteForSign{
931+
ProposedBlockInfo: quorumCert.ProposedBlockInfo,
932+
GapNumber: quorumCert.GapNumber,
933+
}), sig, epochInfo.Masternodes)
934+
if err != nil {
935+
log.Error("[verifyQC] Error while verfying QC message signatures", "Error", err)
936+
return errors.New("error while verfying QC message signatures")
937+
}
938+
if !verified {
939+
log.Warn("[verifyQC] Signature not verified doing QC verification", "QC", quorumCert)
940+
return errors.New("fail to verify QC due to signature mis-match")
941+
}
942+
return nil
943+
}
944+
})
945+
}
946+
err = eg.Wait()
775947

948+
elapsed := time.Since(start)
949+
log.Debug("[verifyQC] time verify message signatures of qc", "elapsed", elapsed)
950+
if err != nil {
951+
return err
952+
}
776953
epochSwitchNumber := epochInfo.EpochSwitchBlockInfo.Number.Uint64()
777954
gapNumber := epochSwitchNumber - epochSwitchNumber%x.config.Epoch
778955
if gapNumber > x.config.Gap {
@@ -790,7 +967,7 @@ func (x *XDPoS_v2) verifyQC(blockChainReader consensus.ChainReader, quorumCert *
790967

791968
// Update local QC variables including highestQC & lockQuorumCert, as well as commit the blocks that satisfy the algorithm requirements
792969
func (x *XDPoS_v2) processQC(blockChainReader consensus.ChainReader, incomingQuorumCert *types.QuorumCert) error {
793-
log.Debug("[processQC][Before]", "HighQC", x.highestQuorumCert.ProposedBlockInfo.Round)
970+
log.Trace("[processQC][Before]", "HighQC", x.highestQuorumCert)
794971
// 1. Update HighestQC
795972
if incomingQuorumCert.ProposedBlockInfo.Round > x.highestQuorumCert.ProposedBlockInfo.Round {
796973
log.Debug("[processQC] update x.highestQuorumCert", "blockNum", incomingQuorumCert.ProposedBlockInfo.Number, "round", incomingQuorumCert.ProposedBlockInfo.Round, "hash", incomingQuorumCert.ProposedBlockInfo.Hash)
@@ -824,7 +1001,7 @@ func (x *XDPoS_v2) processQC(blockChainReader consensus.ChainReader, incomingQuo
8241001
if incomingQuorumCert.ProposedBlockInfo.Round >= x.currentRound {
8251002
x.setNewRound(blockChainReader, incomingQuorumCert.ProposedBlockInfo.Round+1)
8261003
}
827-
log.Debug("[processQC][After]", "HighQC", x.highestQuorumCert.ProposedBlockInfo.Round)
1004+
log.Trace("[processQC][After]", "HighQC", x.highestQuorumCert)
8281005
return nil
8291006
}
8301007

@@ -839,7 +1016,8 @@ func (x *XDPoS_v2) setNewRound(blockChainReader consensus.ChainReader, round typ
8391016
x.currentRound = round
8401017
x.timeoutCount = 0
8411018
x.timeoutWorker.Reset(blockChainReader, x.currentRound, x.highestQuorumCert.ProposedBlockInfo.Round)
842-
// don't need to clean pool, we have other process to clean and it's not good to clean here, some edge case may break
1019+
x.timeoutPool.Clear()
1020+
// don't need to clean vote pool, we have other process to clean and it's not good to clean here, some edge case may break
8431021
// for example round gets bump during collecting vote, so we have to keep vote.
8441022

8451023
// send signal to newRoundCh, but if full don't send
@@ -1072,7 +1250,6 @@ func (x *XDPoS_v2) periodicJob() {
10721250
<-ticker.C
10731251
x.hygieneVotePool()
10741252
x.hygieneTimeoutPool()
1075-
x.hygieneSyncInfoPool()
10761253
}
10771254
}()
10781255
}

0 commit comments

Comments
 (0)