vote and timeout handlers

This commit is contained in:
Jianrong 2021-11-14 16:05:38 +11:00
parent d47d9a2a99
commit a39612e540
8 changed files with 462 additions and 125 deletions

View file

@ -100,7 +100,7 @@ func NewFaker(db ethdb.Database, chainConfig *params.ChainConfig) *XDPoS {
signingTxsCache: signingTxsCache, signingTxsCache: signingTxsCache,
EngineV1: engine_v1.NewFaker(db, conf), EngineV1: engine_v1.NewFaker(db, conf),
EngineV2: engine_v2.NewFaker(db, conf), EngineV2: engine_v2.New(conf, db),
} }
return fakeEngine return fakeEngine
} }

View file

@ -22,18 +22,19 @@ type XDPoS_v2 struct {
config *params.XDPoSConfig // Consensus engine configuration parameters config *params.XDPoSConfig // Consensus engine configuration parameters
db ethdb.Database // Database to store and retrieve snapshot checkpoints db ethdb.Database // Database to store and retrieve snapshot checkpoints
signer common.Address // Ethereum address of the signing key signer common.Address // Ethereum address of the signing key
signFn clique.SignerFn // Signer function to authorize hashes with signFn clique.SignerFn // Signer function to authorize hashes with
lock sync.RWMutex // Protects the signer fields signLock sync.RWMutex // Protects the signer fields
BroadcastCh chan interface{} BroadcastCh chan interface{}
BFTQueue chan interface{}
timeoutWorker *countdown.CountdownTimer // Timer to generate broadcast timeout msg if threashold reached timeoutWorker *countdown.CountdownTimer // Timer to generate broadcast timeout msg if threashold reached
timeoutPool *utils.Pool lock sync.RWMutex // Protects the currentRound fields etc
currentRound utils.Round timeoutPool *utils.Pool
highestVotedRound utils.Round votePool *utils.Pool
highestQuorumCert *utils.QuorumCert currentRound utils.Round
highestVotedRound utils.Round
highestQuorumCert *utils.QuorumCert
// LockQC in XDPoS Consensus 2.0, used in voting rule // LockQC in XDPoS Consensus 2.0, used in voting rule
lockQuorumCert *utils.QuorumCert lockQuorumCert *utils.QuorumCert
highestTimeoutCert *utils.TimeoutCert highestTimeoutCert *utils.TimeoutCert
@ -45,16 +46,22 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 {
duration := time.Duration(config.V2.TimeoutWorkerDuration) * time.Millisecond duration := time.Duration(config.V2.TimeoutWorkerDuration) * time.Millisecond
timer := countdown.NewCountDown(duration) timer := countdown.NewCountDown(duration)
timeoutPool := utils.NewPool(config.V2.CertThreshold) timeoutPool := utils.NewPool(config.V2.CertThreshold)
votePool := utils.NewPool(config.V2.CertThreshold)
engine := &XDPoS_v2{ engine := &XDPoS_v2{
config: config, config: config,
db: db, db: db,
timeoutWorker: timer, timeoutWorker: timer,
BroadcastCh: make(chan interface{}), BroadcastCh: make(chan interface{}),
BFTQueue: make(chan interface{}), timeoutPool: timeoutPool,
timeoutPool: timeoutPool, votePool: votePool,
highestTimeoutCert: &utils.TimeoutCert{},
highestQuorumCert: &utils.QuorumCert{},
} }
// Add callback to the timer // Add callback to the timer
timer.OnTimeoutFn = engine.onCountdownTimeout timer.OnTimeoutFn = engine.onCountdownTimeout
// Attach vote & timeout pool callback function when it reached threshold
votePool.SetOnThresholdFn(engine.onVotePoolThresholdReached)
timeoutPool.SetOnThresholdFn(engine.onTimeoutPoolThresholdReached)
return engine return engine
} }
@ -62,41 +69,23 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 {
/* /*
Testing tools Testing tools
*/ */
// Test only. Never to be used for mainnet implementation func (x *XDPoS_v2) SetNewRoundFaker(newRound utils.Round, resetTimer bool) {
func NewFaker(db ethdb.Database, config *params.XDPoSConfig) *XDPoS_v2 { // Reset a bunch of things
var fakeEngine *XDPoS_v2 if resetTimer {
// Set any missing consensus parameters to their defaults x.timeoutWorker.Reset()
conf := config
// Setup Timer
duration := time.Duration(config.V2.TimeoutWorkerDuration) * time.Millisecond
timer := countdown.NewCountDown(duration)
timeoutPool := utils.NewPool(2)
// Allocate the snapshot caches and create the engine
fakeEngine = &XDPoS_v2{
config: conf,
db: db,
timeoutWorker: timer,
BroadcastCh: make(chan interface{}),
BFTQueue: make(chan interface{}),
timeoutPool: timeoutPool,
} }
// Add callback to the timer x.currentRound = newRound
timer.OnTimeoutFn = fakeEngine.onCountdownTimeout
return fakeEngine
} }
// Test only. // Utils for test to check currentRound value
func (x *XDPoS_v2) SetNewRoundFaker(newRound utils.Round) { func (x *XDPoS_v2) GetCurrentRound() utils.Round {
// Reset a bunch of things return x.currentRound
x.timeoutWorker.Reset()
x.currentRound = newRound
} }
// Authorize injects a private key into the consensus engine to mint new blocks with. // Authorize injects a private key into the consensus engine to mint new blocks with.
func (x *XDPoS_v2) Authorize(signer common.Address, signFn clique.SignerFn) { func (x *XDPoS_v2) Authorize(signer common.Address, signFn clique.SignerFn) {
x.lock.Lock() x.signLock.Lock()
defer x.lock.Unlock() defer x.signLock.Unlock()
x.signer = signer x.signer = signer
x.signFn = signFn x.signFn = signFn
@ -110,22 +99,10 @@ func (x *XDPoS_v2) VerifyHeader(chain consensus.ChainReader, header *types.Heade
return nil return nil
} }
// Push mesages(i.e vote, sync info & timeout) into BFTQueue. This funciton shall be called by BFT protocal manager
func (x *XDPoS_v2) Enqueue() error {
return nil
}
// Main function for the v2 consensus.
func (x *XDPoS_v2) Dispatcher() error {
// 1. Pull message from the BFTQueue and call the relevant handler by message type, such as vote, timeout or syncInfo
// 2. Only 1 message processing at the time
return nil
}
/* /*
SyncInfo workflow SyncInfo workflow
*/ */
// Verify syncInfo and trigger trigger process QC or TC if successful // Verify syncInfo and trigger process QC or TC if successful
func (x *XDPoS_v2) VerifySyncInfoMessage(syncInfo utils.SyncInfo) error { func (x *XDPoS_v2) VerifySyncInfoMessage(syncInfo utils.SyncInfo) error {
/* /*
1. Verify items including: 1. Verify items including:
@ -133,6 +110,16 @@ func (x *XDPoS_v2) VerifySyncInfoMessage(syncInfo utils.SyncInfo) error {
- verifyTC - verifyTC
2. Broadcast(Not part of consensus) 2. Broadcast(Not part of consensus)
*/ */
err := x.verifyQC(&syncInfo.HighestQuorumCert)
if err != nil {
log.Warn("SyncInfo message verification failed due to QC", err)
return err
}
err = x.verifyTC(&syncInfo.HighestTimeoutCert)
if err != nil {
log.Warn("SyncInfo message verification failed due to TC", err)
return err
}
return nil return nil
} }
@ -147,7 +134,7 @@ func (x *XDPoS_v2) SyncInfoHandler(header *types.Header) error {
/* /*
Vote workflow Vote workflow
*/ */
func (x *XDPoS_v2) VerifyVoteMessage(vote utils.Vote) error { func (x *XDPoS_v2) VerifyVoteMessage(vote utils.Vote) (bool, error) {
/* /*
1. Check signature: 1. Check signature:
- Use ecRecover to get the public key - Use ecRecover to get the public key
@ -156,16 +143,51 @@ func (x *XDPoS_v2) VerifyVoteMessage(vote utils.Vote) error {
2. Verify blockInfo 2. Verify blockInfo
3. Broadcast(Not part of consensus) 3. Broadcast(Not part of consensus)
*/ */
return x.verifyMsgSignature(utils.VoteSigHash(&vote.ProposedBlockInfo), vote.Signature)
}
// Consensus entry point for processing vote message to produce QC
func (x *XDPoS_v2) VoteHandler(voteMsg utils.Vote) error {
x.lock.Lock()
defer x.lock.Unlock()
// 1. checkRoundNumber
if voteMsg.ProposedBlockInfo.Round != x.currentRound {
return fmt.Errorf("Vote message round number: %v does not match currentRound: %v", voteMsg.ProposedBlockInfo.Round, x.currentRound)
}
// Collect vote
thresholdReached, numberOfVotesInPool, hookError := x.votePool.Add(&voteMsg)
if hookError != nil {
log.Error("Error while adding vote message to the pool, ", hookError)
return hookError
}
log.Debug("Vote pool threashold reached: %v, number of items in the pool: %v", thresholdReached, numberOfVotesInPool)
return nil return nil
} }
func (x *XDPoS_v2) VoteHandler() { /*
/* Function that will be called by votePool when it reached threshold.
1. checkRoundNumber In the engine v2, we will need to generate and process QC
3. Collect vote (TODO) */
4. Genrate QC (TODO) func (x *XDPoS_v2) onVotePoolThresholdReached(pooledVotes map[common.Hash]utils.PoolObj, currentVoteMsg utils.PoolObj) error {
5. processQC signatures := []utils.Signature{}
*/ for _, v := range pooledVotes {
signatures = append(signatures, v.(*utils.Vote).Signature)
}
// Genrate QC
quorumCert := &utils.QuorumCert{
ProposedBlockInfo: currentVoteMsg.(*utils.Vote).ProposedBlockInfo,
Signatures: signatures,
}
err := x.processQC(quorumCert)
if err != nil {
log.Error("Error while processing QC in the Vote handler after reaching pool threshold, ", err)
return err
}
log.Info("🗳 Successfully processed the vote and produced QC!")
return nil
} }
/* /*
@ -180,37 +202,62 @@ func (x *XDPoS_v2) VoteHandler() {
2. Broadcast(Not part of consensus) 2. Broadcast(Not part of consensus)
*/ */
func (x *XDPoS_v2) VerifyTimeoutMessage(timeoutMsg utils.Timeout) (bool, error) { func (x *XDPoS_v2) VerifyTimeoutMessage(timeoutMsg utils.Timeout) (bool, error) {
// Recover the public key and the Ethereum address return x.verifyMsgSignature(utils.TimeoutSigHash(&timeoutMsg.Round), timeoutMsg.Signature)
pubkey, err := crypto.Ecrecover(utils.TimeoutSigHash(&timeoutMsg.Round).Bytes(), timeoutMsg.Signature)
if err != nil {
return false, fmt.Errorf("Error while verifying time out message: %v", err)
}
var signerAddress common.Address
copy(signerAddress[:], crypto.Keccak256(pubkey[1:])[12:])
masternodes := x.getCurrentRoundMasterNodes()
for _, mn := range masternodes {
if mn == signerAddress {
return true, nil
}
}
return false, fmt.Errorf("Masternodes does not contain signer address. Master node list %v, Signer address: %v", masternodes, signerAddress)
} }
/* /*
Entry point for handling timeout message to process below:
1. checkRoundNumber() 1. checkRoundNumber()
2. Collect timeout (TODO) 2. Collect timeout
3. Genrate TC (TODO) Once timeout pool reached threshold, it will trigger the call to the hook function "onTimeoutPoolThresholdReached"
4. processTC()
5. generateSyncInfo()
*/ */
func (x *XDPoS_v2) TimeoutHandler(timeout *utils.Timeout) { func (x *XDPoS_v2) TimeoutHandler(timeout *utils.Timeout) error {
// Collect timeout, generate TC x.lock.Lock()
timeoutCert := x.timeoutPool.Add(timeout) defer x.lock.Unlock()
// If TC is generated
if timeoutCert != nil { // 1. checkRoundNumber
//TODO: processTC(),generateSyncInfo() if timeout.Round != x.currentRound {
return fmt.Errorf("Timeout message round number: %v does not match currentRound: %v", timeout.Round, x.currentRound)
} }
// Collect timeout, generate TC
isThresholdReached, numberOfTimeoutsInPool, hookError := x.timeoutPool.Add(timeout)
if hookError != nil {
log.Error("Error adding timeout to the pool, ", hookError.Error())
return hookError
}
log.Debug("Timeout pool threashold reached: %v, number of items in the pool: %v", isThresholdReached, numberOfTimeoutsInPool)
return nil
}
/*
Function that will be called by timeoutPool when it reached threshold.
In the engine v2, we will need to:
1. Genrate TC
2. processTC()
3. generateSyncInfo()
*/
func (x *XDPoS_v2) onTimeoutPoolThresholdReached(pooledTimeouts map[common.Hash]utils.PoolObj, currentTimeoutMsg utils.PoolObj) error {
signatures := []utils.Signature{}
for _, v := range pooledTimeouts {
signatures = append(signatures, v.(*utils.Timeout).Signature)
}
// Genrate TC
timeoutCert := &utils.TimeoutCert{
Round: currentTimeoutMsg.(*utils.Timeout).Round,
Signatures: signatures,
}
// Process TC
err := x.processTC(timeoutCert)
if err != nil {
log.Error("Error while processing TC in the Timeout handler after reaching pool threshold, ", err.Error())
return err
}
// Generate and broadcast syncInfo
syncInfo := x.getSyncInfo()
x.broadcastToBftChannel(syncInfo)
log.Info("⏰ Successfully processed the timeout message and produced TC & SyncInfo!")
return nil
} }
/* /*
@ -239,7 +286,7 @@ func (x *XDPoS_v2) VerifyBlockInfo(blockInfo utils.BlockInfo) error {
return nil return nil
} }
func (x *XDPoS_v2) verifyQC(header *types.Header) error { func (x *XDPoS_v2) verifyQC(quorumCert *utils.QuorumCert) error {
/* /*
1. Verify signer signatures: (List of signatures) 1. Verify signer signatures: (List of signatures)
- Use ecRecover to get the public key - Use ecRecover to get the public key
@ -250,7 +297,7 @@ func (x *XDPoS_v2) verifyQC(header *types.Header) error {
return nil return nil
} }
func (x *XDPoS_v2) verifyTC(header *types.Header) error { func (x *XDPoS_v2) verifyTC(timeoutCert *utils.TimeoutCert) error {
/* /*
1. Verify signer signature: (List of signatures) 1. Verify signer signature: (List of signatures)
- Use ecRecover to get the public key - Use ecRecover to get the public key
@ -289,7 +336,10 @@ func (x *XDPoS_v2) processTC(timeoutCert *utils.TimeoutCert) error {
x.highestTimeoutCert = timeoutCert x.highestTimeoutCert = timeoutCert
} }
if timeoutCert.Round >= x.currentRound { if timeoutCert.Round >= x.currentRound {
x.setNewRound(timeoutCert.Round + 1) err := x.setNewRound(timeoutCert.Round + 1)
if err != nil {
return err
}
} }
return nil return nil
} }
@ -302,17 +352,12 @@ func (x *XDPoS_v2) processTC(timeoutCert *utils.TimeoutCert) error {
func (x *XDPoS_v2) setNewRound(round utils.Round) error { func (x *XDPoS_v2) setNewRound(round utils.Round) error {
x.currentRound = round x.currentRound = round
//TODO: tell miner now it's a new round and start mine if it's leader //TODO: tell miner now it's a new round and start mine if it's leader
//TODO: reset timer x.timeoutWorker.Reset()
//TODO: vote pools //TODO: vote pools
x.timeoutPool.Clear() x.timeoutPool.Clear()
return nil return nil
} }
// Verify round number against node's local round number(Should be equal)
func (x *XDPoS_v2) checkRoundNumber(header *types.Header) error {
return nil
}
// Hot stuff rule to decide whether this node is eligible to vote for the received block // Hot stuff rule to decide whether this node is eligible to vote for the received block
func (x *XDPoS_v2) verifyVotingRule(header *types.Header) error { func (x *XDPoS_v2) verifyVotingRule(header *types.Header) error {
/* /*
@ -326,10 +371,19 @@ func (x *XDPoS_v2) verifyVotingRule(header *types.Header) error {
} }
// Once Hot stuff voting rule has verified, this node can then send vote // Once Hot stuff voting rule has verified, this node can then send vote
func (x *XDPoS_v2) sendVote(header *types.Header) error { func (x *XDPoS_v2) sendVote(blockInfo utils.BlockInfo) error {
// First step: Generate the signature by using node's private key(The signature is the blockInfo signature) // First step: Generate the signature by using node's private key(The signature is the blockInfo signature)
// Second step: Construct the vote struct with the above signature & blockinfo struct // Second step: Construct the vote struct with the above signature & blockinfo struct
// Third step: Send the vote to broadcast channel // Third step: Send the vote to broadcast channel
signedHash, err := x.signSignature(utils.VoteSigHash(&blockInfo))
if err != nil {
return err
}
voteMsg := &utils.Vote{
ProposedBlockInfo: blockInfo,
Signature: signedHash,
}
x.broadcastToBftChannel(voteMsg)
return nil return nil
} }
@ -340,14 +394,9 @@ func (x *XDPoS_v2) sendVote(header *types.Header) error {
3. send to broadcast channel 3. send to broadcast channel
*/ */
func (x *XDPoS_v2) sendTimeout() error { func (x *XDPoS_v2) sendTimeout() error {
// Don't hold the signer fields for the entire sealing procedure signedHash, err := x.signSignature(utils.TimeoutSigHash(&x.currentRound))
x.lock.RLock()
signer, signFn := x.signer, x.signFn
x.lock.RUnlock()
signedHash, err := signFn(accounts.Account{Address: signer}, utils.TimeoutSigHash(&x.currentRound).Bytes())
if err != nil { if err != nil {
return fmt.Errorf("Error while signing for timeout message") return err
} }
timeoutMsg := &utils.Timeout{ timeoutMsg := &utils.Timeout{
Round: x.currentRound, Round: x.currentRound,
@ -362,11 +411,45 @@ func (x *XDPoS_v2) sendSyncInfo() error {
return nil return nil
} }
func (x *XDPoS_v2) signSignature(signingHash common.Hash) (utils.Signature, error) {
// Don't hold the signFn for the whole signing operation
x.signLock.RLock()
signer, signFn := x.signer, x.signFn
x.signLock.RUnlock()
signedHash, err := signFn(accounts.Account{Address: signer}, signingHash.Bytes())
if err != nil {
return nil, fmt.Errorf("Error while signing hash")
}
return signedHash, nil
}
func (x *XDPoS_v2) verifyMsgSignature(signedHashToBeVerified common.Hash, signature utils.Signature) (bool, error) {
// Recover the public key and the Ethereum address
pubkey, err := crypto.Ecrecover(signedHashToBeVerified.Bytes(), signature)
if err != nil {
return false, fmt.Errorf("Error while verifying message: %v", err)
}
var signerAddress common.Address
copy(signerAddress[:], crypto.Keccak256(pubkey[1:])[12:])
masternodes := x.getCurrentRoundMasterNodes()
for _, mn := range masternodes {
if mn == signerAddress {
return true, nil
}
}
return false, fmt.Errorf("Masternodes does not contain signer address. Master node list %v, Signer address: %v", masternodes, signerAddress)
}
/* /*
Function that will be called by timer when countdown reaches its threshold. Function that will be called by timer when countdown reaches its threshold.
In the engine v2, we would need to broadcast timeout messages to other peers In the engine v2, we would need to broadcast timeout messages to other peers
*/ */
func (x *XDPoS_v2) onCountdownTimeout(time time.Time) error { func (x *XDPoS_v2) onCountdownTimeout(time time.Time) error {
x.lock.Lock()
defer x.lock.Unlock()
err := x.sendTimeout() err := x.sendTimeout()
if err != nil { if err != nil {
log.Error("Error while sending out timeout message at time: ", time) log.Error("Error while sending out timeout message at time: ", time)
@ -376,9 +459,18 @@ func (x *XDPoS_v2) onCountdownTimeout(time time.Time) error {
} }
func (x *XDPoS_v2) broadcastToBftChannel(msg interface{}) { func (x *XDPoS_v2) broadcastToBftChannel(msg interface{}) {
x.BroadcastCh <- msg go func() {
x.BroadcastCh <- msg
}()
} }
func (x *XDPoS_v2) getCurrentRoundMasterNodes() []common.Address { func (x *XDPoS_v2) getCurrentRoundMasterNodes() []common.Address {
return []common.Address{} return []common.Address{}
} }
func (x *XDPoS_v2) getSyncInfo() utils.SyncInfo {
return utils.SyncInfo{
HighestQuorumCert: *x.highestQuorumCert,
HighestTimeoutCert: *x.highestTimeoutCert,
}
}

View file

@ -13,7 +13,7 @@ type PoolObj interface {
type Pool struct { type Pool struct {
objList map[string]map[common.Hash]PoolObj objList map[string]map[common.Hash]PoolObj
threshold int threshold int
onThresholdFn func(map[common.Hash]PoolObj) error onThresholdFn func(objsInPool map[common.Hash]PoolObj, currentObj PoolObj) error
} }
func NewPool(threshold int) *Pool { func NewPool(threshold int) *Pool {
@ -23,7 +23,8 @@ func NewPool(threshold int) *Pool {
} }
} }
func (p *Pool) Add(obj PoolObj) error { // call the hook function onThresholdFn if reached threshold and return boolean to indicate whether pool has reached threshold
func (p *Pool) Add(obj PoolObj) (bool, int, error) {
poolKey := obj.PoolKey() poolKey := obj.PoolKey()
objListKeyed, ok := p.objList[poolKey] objListKeyed, ok := p.objList[poolKey]
if !ok { if !ok {
@ -31,15 +32,16 @@ func (p *Pool) Add(obj PoolObj) error {
objListKeyed = p.objList[poolKey] objListKeyed = p.objList[poolKey]
} }
objListKeyed[obj.Hash()] = obj objListKeyed[obj.Hash()] = obj
if len(objListKeyed) >= p.threshold { numOfItems := len(objListKeyed)
if numOfItems >= p.threshold {
delete(p.objList, poolKey) delete(p.objList, poolKey)
if p.onThresholdFn != nil { if p.onThresholdFn != nil {
return p.onThresholdFn(objListKeyed) return true, numOfItems, p.onThresholdFn(objListKeyed, obj)
} else { } else {
return fmt.Errorf("no call back function for pool") return true, numOfItems, fmt.Errorf("no call back function for pool")
} }
} }
return nil return false, numOfItems, nil
} }
func (p *Pool) Clear() { func (p *Pool) Clear() {
@ -50,6 +52,6 @@ func (p *Pool) SetThreshold(t int) {
p.threshold = t p.threshold = t
} }
func (p *Pool) SetOnThresholdFn(f func(map[common.Hash]PoolObj) error) { func (p *Pool) SetOnThresholdFn(f func(objsInPool map[common.Hash]PoolObj, currentObj PoolObj) error) {
p.onThresholdFn = f p.onThresholdFn = f
} }

View file

@ -11,7 +11,7 @@ import (
func TestPoolWithTimeout(t *testing.T) { func TestPoolWithTimeout(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var ret int var ret int
onThresholdFn := func(po map[common.Hash]PoolObj) error { onThresholdFn := func(po map[common.Hash]PoolObj, currentPoolObj PoolObj) error {
for _, m := range po { for _, m := range po {
if _, ok := m.(*Timeout); ok { if _, ok := m.(*Timeout); ok {
ret += 1 ret += 1
@ -49,7 +49,7 @@ func TestPoolWithTimeout(t *testing.T) {
func TestPoolWithVote(t *testing.T) { func TestPoolWithVote(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
var ret int var ret int
onThresholdFn := func(po map[common.Hash]PoolObj) error { onThresholdFn := func(po map[common.Hash]PoolObj, currentPoolObj PoolObj) error {
for _, m := range po { for _, m := range po {
if _, ok := m.(*Vote); ok { if _, ok := m.(*Vote); ok {
ret += 1 ret += 1

View file

@ -62,6 +62,7 @@ type PublicApiSnapshot struct {
// Round number type in XDPoS 2.0 // Round number type in XDPoS 2.0
type Round uint64 type Round uint64
type Signature []byte
// Block Info struct in XDPoS 2.0, used for vote message, etc. // Block Info struct in XDPoS 2.0, used for vote message, etc.
type BlockInfo struct { type BlockInfo struct {
@ -73,13 +74,13 @@ type BlockInfo struct {
// Vote message in XDPoS 2.0 // Vote message in XDPoS 2.0
type Vote struct { type Vote struct {
ProposedBlockInfo BlockInfo ProposedBlockInfo BlockInfo
Signature []byte Signature Signature
} }
// Timeout message in XDPoS 2.0 // Timeout message in XDPoS 2.0
type Timeout struct { type Timeout struct {
Round Round Round Round
Signature []byte Signature Signature
} }
// BFT Sync Info message in XDPoS 2.0 // BFT Sync Info message in XDPoS 2.0
@ -91,13 +92,13 @@ type SyncInfo struct {
// Quorum Certificate struct in XDPoS 2.0 // Quorum Certificate struct in XDPoS 2.0
type QuorumCert struct { type QuorumCert struct {
ProposedBlockInfo BlockInfo ProposedBlockInfo BlockInfo
Signatures [][]byte Signatures []Signature
} }
// Timeout Certificate struct in XDPoS 2.0 // Timeout Certificate struct in XDPoS 2.0
type TimeoutCert struct { type TimeoutCert struct {
Round Round Round Round
Signatures [][]byte Signatures []Signature
} }
// The parsed extra fields in block header in XDPoS 2.0 (excluding the version byte) // The parsed extra fields in block header in XDPoS 2.0 (excluding the version byte)

View file

@ -12,7 +12,7 @@ func toyExtraFields() *ExtraFields_v2 {
round := Round(307) round := Round(307)
blockInfo := BlockInfo{Hash: common.BigToHash(big.NewInt(2047)), Round: round - 1, Number: big.NewInt(1)} blockInfo := BlockInfo{Hash: common.BigToHash(big.NewInt(2047)), Round: round - 1, Number: big.NewInt(1)}
signature := []byte{1, 2, 3, 4, 5, 6, 7, 8} signature := []byte{1, 2, 3, 4, 5, 6, 7, 8}
signatures := [][]byte{signature} signatures := []Signature{signature}
quorumCert := QuorumCert{ProposedBlockInfo: blockInfo, Signatures: signatures} quorumCert := QuorumCert{ProposedBlockInfo: blockInfo, Signatures: signatures}
e := &ExtraFields_v2{Round: round, QuorumCert: quorumCert} e := &ExtraFields_v2{Round: round, QuorumCert: quorumCert}
return e return e
@ -39,9 +39,9 @@ func TestHashAndSigHash(t *testing.T) {
blockInfo2 := BlockInfo{Hash: common.BigToHash(big.NewInt(4095)), Round: round - 1, Number: big.NewInt(1)} blockInfo2 := BlockInfo{Hash: common.BigToHash(big.NewInt(4095)), Round: round - 1, Number: big.NewInt(1)}
signature1 := []byte{1, 2, 3, 4, 5, 6, 7, 8} signature1 := []byte{1, 2, 3, 4, 5, 6, 7, 8}
signature2 := []byte{1, 2, 3, 4, 5, 6, 7, 7} signature2 := []byte{1, 2, 3, 4, 5, 6, 7, 7}
signatures1 := [][]byte{signature1} signatures1 := []Signature{signature1}
quorumCert1 := QuorumCert{ProposedBlockInfo: blockInfo1, Signatures: signatures1} quorumCert1 := QuorumCert{ProposedBlockInfo: blockInfo1, Signatures: signatures1}
signatures2 := [][]byte{signature2} signatures2 := []Signature{signature2}
quorumCert2 := QuorumCert{ProposedBlockInfo: blockInfo1, Signatures: signatures2} quorumCert2 := QuorumCert{ProposedBlockInfo: blockInfo1, Signatures: signatures2}
vote1 := Vote{ProposedBlockInfo: blockInfo1, Signature: signature1} vote1 := Vote{ProposedBlockInfo: blockInfo1, Signature: signature1}
vote2 := Vote{ProposedBlockInfo: blockInfo1, Signature: signature2} vote2 := Vote{ProposedBlockInfo: blockInfo1, Signature: signature2}

View file

@ -41,7 +41,7 @@ var (
} }
TestXDPoSV2Config = &V2{ TestXDPoSV2Config = &V2{
TimeoutWorkerDuration: 5000, TimeoutWorkerDuration: 5000,
CertThreshold: 2, CertThreshold: 3,
} }
// XDPoSChain mainnet config // XDPoSChain mainnet config

View file

@ -1,8 +1,10 @@
package consensus package consensus
import ( import (
"math/big"
"testing" "testing"
"github.com/XinFinOrg/XDPoSChain/common"
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS"
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils" "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
"github.com/XinFinOrg/XDPoSChain/params" "github.com/XinFinOrg/XDPoSChain/params"
@ -13,7 +15,7 @@ func TestCountdownTimeoutToSendTimeoutMessage(t *testing.T) {
blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine) blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2 engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
engineV2.SetNewRoundFaker(utils.Round(1)) engineV2.SetNewRoundFaker(utils.Round(1), true)
timeoutMsg := <-engineV2.BroadcastCh timeoutMsg := <-engineV2.BroadcastCh
assert.NotNil(t, timeoutMsg) assert.NotNil(t, timeoutMsg)
@ -24,3 +26,243 @@ func TestCountdownTimeoutToSendTimeoutMessage(t *testing.T) {
// This shows we are able to decode the timeout message, which is what this test is all about // This shows we are able to decode the timeout message, which is what this test is all about
assert.Regexp(t, "^Masternodes does not contain signer addres.*", err.Error()) assert.Regexp(t, "^Masternodes does not contain signer addres.*", err.Error())
} }
// Timeout handler
func TestTimeoutMessageHandlerSuccessfullyGenerateTCandSyncInfo(t *testing.T) {
blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
// Set round to 1
engineV2.SetNewRoundFaker(utils.Round(1), false)
// Create two timeout message which will not reach timeout pool threshold
timeoutMsg := &utils.Timeout{
Round: utils.Round(1),
Signature: []byte{1},
}
err := engineV2.TimeoutHandler(timeoutMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(1), engineV2.GetCurrentRound())
timeoutMsg = &utils.Timeout{
Round: utils.Round(1),
Signature: []byte{2},
}
err = engineV2.TimeoutHandler(timeoutMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(1), engineV2.GetCurrentRound())
// Create a timeout message that should trigger timeout pool hook
timeoutMsg = &utils.Timeout{
Round: utils.Round(1),
Signature: []byte{3},
}
err = engineV2.TimeoutHandler(timeoutMsg)
assert.Nil(t, err)
syncInfoMsg := <-engineV2.BroadcastCh
assert.NotNil(t, syncInfoMsg)
// Should have QC, however, we did not inilise it, hence will show default empty value
qc := syncInfoMsg.(utils.SyncInfo).HighestQuorumCert
assert.NotNil(t, qc)
tc := syncInfoMsg.(utils.SyncInfo).HighestTimeoutCert
assert.NotNil(t, tc)
assert.Equal(t, tc.Round, utils.Round(1))
sigatures := []utils.Signature{[]byte{1}, []byte{2}, []byte{3}}
assert.ElementsMatch(t, tc.Signatures, sigatures)
assert.Equal(t, utils.Round(2), engineV2.GetCurrentRound())
}
func TestThrowErrorIfTimeoutMsgRoundLessThanCurrentRound(t *testing.T) {
blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
// Set round to 3
engineV2.SetNewRoundFaker(utils.Round(3), false)
// Create two timeout message which will not reach timeout pool threshold
timeoutMsg := &utils.Timeout{
Round: utils.Round(2),
Signature: []byte{1},
}
err := engineV2.TimeoutHandler(timeoutMsg)
assert.NotNil(t, err)
// Timeout msg round > currentRound
assert.Equal(t, "Timeout message round number: 2 does not match currentRound: 3", err.Error())
// Set round to 1
engineV2.SetNewRoundFaker(utils.Round(1), false)
err = engineV2.TimeoutHandler(timeoutMsg)
assert.NotNil(t, err)
// Timeout msg round < currentRound
assert.Equal(t, "Timeout message round number: 2 does not match currentRound: 1", err.Error())
}
// VoteHandler
func TestVoteMessageHandlerSuccessfullyGenerateTCandSyncInfo(t *testing.T) {
blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
blockInfo := &utils.BlockInfo{
Hash: common.HexToHash("0x1"),
Round: utils.Round(1),
Number: big.NewInt(999),
}
// Set round to 1
engineV2.SetNewRoundFaker(utils.Round(1), false)
// Create two timeout message which will not reach vote pool threshold
voteMsg := &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{1},
}
err := engineV2.VoteHandler(*voteMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(1), engineV2.GetCurrentRound())
voteMsg = &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{2},
}
err = engineV2.VoteHandler(*voteMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(1), engineV2.GetCurrentRound())
// Create a vote message that should trigger vite pool hook
voteMsg = &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{3},
}
err = engineV2.VoteHandler(*voteMsg)
assert.Nil(t, err)
// Check round has now changed from 1 to 2
assert.Equal(t, utils.Round(2), engineV2.GetCurrentRound())
}
func TestThrowErrorIfVoteMsgRoundLessThanCurrentRound(t *testing.T) {
blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
blockInfo := &utils.BlockInfo{
Hash: common.HexToHash("0x1"),
Round: utils.Round(2),
Number: big.NewInt(999),
}
// Set round to 3
engineV2.SetNewRoundFaker(utils.Round(3), false)
// Create two timeout message which will not reach timeout pool threshold
voteMsg := &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{1},
}
// voteRound > currentRound
err := engineV2.VoteHandler(*voteMsg)
assert.NotNil(t, err)
assert.Equal(t, "Vote message round number: 2 does not match currentRound: 3", err.Error())
// Set round to 1
engineV2.SetNewRoundFaker(utils.Round(1), false)
err = engineV2.VoteHandler(*voteMsg)
assert.NotNil(t, err)
// voteRound < currentRound
assert.Equal(t, "Vote message round number: 2 does not match currentRound: 1", err.Error())
}
func TestProcessVoteMsgThenTimeoutMsg(t *testing.T) {
blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine)
engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2
// Set round to 1
engineV2.SetNewRoundFaker(utils.Round(1), false)
// Start with vote messages
blockInfo := &utils.BlockInfo{
Hash: common.HexToHash("0x1"),
Round: utils.Round(1),
Number: big.NewInt(999),
}
// Create two timeout message which will not reach vote pool threshold
voteMsg := &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{1},
}
err := engineV2.VoteHandler(*voteMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(1), engineV2.GetCurrentRound())
voteMsg = &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{2},
}
err = engineV2.VoteHandler(*voteMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(1), engineV2.GetCurrentRound())
// Create a vote message that should trigger vite pool hook
voteMsg = &utils.Vote{
ProposedBlockInfo: *blockInfo,
Signature: []byte{3},
}
err = engineV2.VoteHandler(*voteMsg)
assert.Nil(t, err)
// Check round has now changed from 1 to 2
assert.Equal(t, utils.Round(2), engineV2.GetCurrentRound())
// We shall have highestQuorumCert in engine now, let's do timeout msg to see if we can broadcast SyncInfo which contains both highestQuorumCert and HighestTimeoutCert
// First, all incoming old timeout msg shall not be processed
timeoutMsg := &utils.Timeout{
Round: utils.Round(1),
Signature: []byte{1},
}
err = engineV2.TimeoutHandler(timeoutMsg)
assert.NotNil(t, err)
assert.Equal(t, "Timeout message round number: 1 does not match currentRound: 2", err.Error())
// Ok, let's do the timeout msg which is on the same round as the current round by creating two timeout message which will not reach timeout pool threshold
timeoutMsg = &utils.Timeout{
Round: utils.Round(2),
Signature: []byte{1},
}
err = engineV2.TimeoutHandler(timeoutMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(2), engineV2.GetCurrentRound())
timeoutMsg = &utils.Timeout{
Round: utils.Round(2),
Signature: []byte{2},
}
err = engineV2.TimeoutHandler(timeoutMsg)
assert.Nil(t, err)
assert.Equal(t, utils.Round(2), engineV2.GetCurrentRound())
// Create a timeout message that should trigger timeout pool hook
timeoutMsg = &utils.Timeout{
Round: utils.Round(2),
Signature: []byte{3},
}
err = engineV2.TimeoutHandler(timeoutMsg)
assert.Nil(t, err)
syncInfoMsg := <-engineV2.BroadcastCh
assert.NotNil(t, syncInfoMsg)
// Should have HighestQuorumCert from previous round votes
qc := syncInfoMsg.(utils.SyncInfo).HighestQuorumCert
assert.NotNil(t, qc)
assert.Equal(t, utils.Round(1), qc.ProposedBlockInfo.Round)
tc := syncInfoMsg.(utils.SyncInfo).HighestTimeoutCert
assert.NotNil(t, tc)
assert.Equal(t, tc.Round, utils.Round(2))
sigatures := []utils.Signature{[]byte{1}, []byte{2}, []byte{3}}
assert.ElementsMatch(t, tc.Signatures, sigatures)
// Round shall be +1 now
assert.Equal(t, utils.Round(3), engineV2.GetCurrentRound())
}