diff --git a/consensus/XDPoS/engines/engine_v2/engine.go b/consensus/XDPoS/engines/engine_v2/engine.go index 4764da379a..cf5c13e56e 100644 --- a/consensus/XDPoS/engines/engine_v2/engine.go +++ b/consensus/XDPoS/engines/engine_v2/engine.go @@ -2,6 +2,7 @@ package engine_v2 import ( "fmt" + "math/big" "sync" "time" @@ -56,6 +57,7 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 { votePool: votePool, highestTimeoutCert: &utils.TimeoutCert{}, highestQuorumCert: &utils.QuorumCert{}, + highestVotedRound: utils.Round(0), } // Add callback to the timer timer.OnTimeoutFn = engine.onCountdownTimeout @@ -124,12 +126,16 @@ func (x *XDPoS_v2) VerifySyncInfoMessage(syncInfo *utils.SyncInfo) error { return nil } -func (x *XDPoS_v2) SyncInfoHandler(syncInfo *utils.SyncInfo) error { +func (x *XDPoS_v2) SyncInfoHandler(chain consensus.ChainReader, syncInfo *utils.SyncInfo) error { /* 1. processQC 2. processTC */ - return nil + err := x.processQC(chain, syncInfo.HighestQuorumCert) + if err != nil { + return nil + } + return x.processTC(syncInfo.HighestTimeoutCert) } /* @@ -267,26 +273,38 @@ func (x *XDPoS_v2) onTimeoutPoolThresholdReached(pooledTimeouts map[common.Hash] } /* - Process Block workflow + Proposed Block workflow */ -func (x *XDPoS_v2) ProcessBlockHandler() { +func (x *XDPoS_v2) ProposedBlockHandler(blockCahinReader consensus.ChainReader, blockInfo *utils.BlockInfo, quorumCert *utils.QuorumCert) error { + x.lock.Lock() + defer x.lock.Unlock() + /* 1. processQC() 2. verifyVotingRule() 3. sendVote() - */ + err := x.processQC(blockCahinReader, quorumCert) + if err != nil { + return err + } + verified, err := x.verifyVotingRule(blockCahinReader, blockInfo, quorumCert) + if err != nil { + return err + } + if verified { + return x.sendVote(blockInfo) + } else { + log.Info("Failed to pass the voting rule verification", "ProposeBlockHash", blockInfo.Hash) + } + + return nil } /* QC & TC Utils */ -// Genrate blockInfo which contains Hash, round and blockNumber and send to queue -func (x *XDPoS_v2) generateBlockInfo() error { - return nil -} - // To be used by different message verification. Verify local DB block info against the received block information(i.e hash, blockNum, round) func (x *XDPoS_v2) VerifyBlockInfo(blockInfo *utils.BlockInfo) error { return nil @@ -317,7 +335,6 @@ func (x *XDPoS_v2) verifyTC(timeoutCert *utils.TimeoutCert) error { func (x *XDPoS_v2) processQC(blockCahinReader consensus.ChainReader, quorumCert *utils.QuorumCert) error { // 1. Update HighestQC if x.highestQuorumCert == nil || (quorumCert.ProposedBlockInfo.Round > x.highestQuorumCert.ProposedBlockInfo.Round) { - //TODO: do I need a clone? x.highestQuorumCert = quorumCert } // 2. Get QC from header and update lockQuorumCert(lockQuorumCert is the parent of highestQC) @@ -376,15 +393,29 @@ func (x *XDPoS_v2) setNewRound(round utils.Round) error { } // 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(blockCahinReader consensus.ChainReader, blockInfo *utils.BlockInfo, quorumCert *utils.QuorumCert) (bool, error) { + // Make sure this node has not voted for this round. We can have a variable highestVotedRound, and check currentRound > highestVotedRound. + if x.currentRound <= x.highestVotedRound { + return false, nil + } /* - Make sure this node has not voted for this round. We can have a variable highestVotedRound, and check currentRound > highestVotedRound. HotStuff Voting rule: header's round == local current round, AND (one of the following two:) header's block extends lockQuorumCert's ProposedBlockInfo (we need a isExtending(block_a, block_b) function), OR header's QC's ProposedBlockInfo.Round > lockQuorumCert's ProposedBlockInfo.Round */ - return nil + if blockInfo.Round != x.currentRound { + return false, nil + } + isExtended, err := x.isExtendingFromAncestor(blockCahinReader, blockInfo, &x.lockQuorumCert.ProposedBlockInfo) + if err != nil { + return false, err + } + if isExtended || (quorumCert.ProposedBlockInfo.Round > x.lockQuorumCert.ProposedBlockInfo.Round) { + return true, nil + } + + return false, nil } // Once Hot stuff voting rule has verified, this node can then send vote @@ -392,6 +423,7 @@ 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) // Second step: Construct the vote struct with the above signature & blockinfo struct // Third step: Send the vote to broadcast channel + // Forth step: Update the highest Voted round signedHash, err := x.signSignature(utils.VoteSigHash(blockInfo)) if err != nil { return err @@ -401,6 +433,7 @@ func (x *XDPoS_v2) sendVote(blockInfo *utils.BlockInfo) error { Signature: signedHash, } x.broadcastToBftChannel(voteMsg) + x.highestVotedRound = x.currentRound return nil } @@ -519,3 +552,22 @@ func (x *XDPoS_v2) commitBlocks(blockCahinReader consensus.ChainReader, proposed return true, nil } + +func (x *XDPoS_v2) isExtendingFromAncestor(blockCahinReader consensus.ChainReader, currentBlock *utils.BlockInfo, ancestorBlock *utils.BlockInfo) (bool, error) { + blockNumDiff := int(big.NewInt(0).Sub(currentBlock.Number, ancestorBlock.Number).Int64()) + + nextBlockHash := currentBlock.Hash + for i := 0; i < blockNumDiff; i++ { + parentBlock := blockCahinReader.GetHeaderByHash(nextBlockHash) + if parentBlock == nil { + return false, fmt.Errorf("Could not find its parent block when checking whether currentBlock %v is extending from the ancestorBlock %v", currentBlock.Number, ancestorBlock.Number) + } else { + nextBlockHash = parentBlock.ParentHash + } + } + + if nextBlockHash == ancestorBlock.Hash { + return true, nil + } + return false, nil +} diff --git a/eth/bfter/bft.go b/eth/bfter/bft.go index 7fb34c1fd3..7cfc2d7d63 100644 --- a/eth/bfter/bft.go +++ b/eth/bfter/bft.go @@ -39,7 +39,7 @@ type ConsensusFns struct { timeoutHandler func(*utils.Timeout) error verifySyncInfo func(*utils.SyncInfo) error - syncInfoHandler func(*utils.SyncInfo) error + syncInfoHandler func(consensus.ChainReader, *utils.SyncInfo) error } type BroadcastFns struct { @@ -136,7 +136,7 @@ func (b *Bfter) SyncInfo(syncInfo *utils.SyncInfo) error { b.knownSyncInfos.Add(syncInfo.Hash(), true) b.broadcastCh <- syncInfo - err = b.consensus.syncInfoHandler(syncInfo) + err = b.consensus.syncInfoHandler(b.blockCahinReader, syncInfo) if err != nil { log.Error("handle BFT SyncInfo", "error", err) return err diff --git a/tests/consensus/countdown_test.go b/tests/consensus/countdown_test.go new file mode 100644 index 0000000000..e0f0f9418f --- /dev/null +++ b/tests/consensus/countdown_test.go @@ -0,0 +1,26 @@ +package consensus + +import ( + "testing" + + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/stretchr/testify/assert" +) + +func TestCountdownTimeoutToSendTimeoutMessage(t *testing.T) { + blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine) + engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2 + + engineV2.SetNewRoundFaker(utils.Round(1), true) + + timeoutMsg := <-engineV2.BroadcastCh + assert.NotNil(t, timeoutMsg) + + valid, err := engineV2.VerifyTimeoutMessage(timeoutMsg.(*utils.Timeout)) + // We can only test valid = false for now as the implementation for getCurrentRoundMasterNodes is not complete + assert.False(t, valid) + // 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()) +} diff --git a/tests/consensus/proposed_block_test.go b/tests/consensus/proposed_block_test.go new file mode 100644 index 0000000000..d9d0c75b89 --- /dev/null +++ b/tests/consensus/proposed_block_test.go @@ -0,0 +1,45 @@ +package consensus + +import ( + "math/big" + "testing" + + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/stretchr/testify/assert" +) + +// ProposeBlock handler +func TestProposedBlockMessageHandlerSuccessfullyGenerateVote(t *testing.T) { + blockchain, _, currentBlock, _ := PrepareXDCTestBlockChainForV2Engine(t, 11, params.TestXDPoSMockChainConfigWithV2Engine) + engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2 + + // Set round to 11 + engineV2.SetNewRoundFaker(utils.Round(11), false) + + var extraField utils.ExtraFields_v2 + err := utils.DecodeBytesExtraFields(currentBlock.Extra(), &extraField) + if err != nil { + t.Fatal("Fail to decode extra data", err) + } + + proposedBlockInfo := &utils.BlockInfo{ + Hash: currentBlock.Hash(), + Round: utils.Round(12), + Number: big.NewInt(12), + } + + err = engineV2.ProposedBlockHandler(blockchain, proposedBlockInfo, &extraField.QuorumCert) + if err != nil { + t.Fatal("Fail propose proposedBlock handler", err) + } + + voteMsg := <-engineV2.BroadcastCh + assert.NotNil(t, voteMsg) + assert.Equal(t, proposedBlockInfo.Hash, voteMsg.(*utils.Vote).ProposedBlockInfo.Hash) + + round, _, highestQC := engineV2.GetProperties() + assert.Equal(t, utils.Round(12), round) + assert.Equal(t, extraField.QuorumCert.Signatures, highestQC.Signatures) +} diff --git a/tests/consensus/timeout_test.go b/tests/consensus/timeout_test.go new file mode 100644 index 0000000000..119da0bf3d --- /dev/null +++ b/tests/consensus/timeout_test.go @@ -0,0 +1,86 @@ +package consensus + +import ( + "testing" + + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS" + "github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils" + "github.com/XinFinOrg/XDPoSChain/params" + "github.com/stretchr/testify/assert" +) + +// 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) + currentRound, _, _ := engineV2.GetProperties() + assert.Equal(t, utils.Round(1), currentRound) + timeoutMsg = &utils.Timeout{ + Round: utils.Round(1), + Signature: []byte{2}, + } + err = engineV2.TimeoutHandler(timeoutMsg) + assert.Nil(t, err) + currentRound, _, _ = engineV2.GetProperties() + assert.Equal(t, utils.Round(1), currentRound) + // 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 + + currentRound, _, _ = engineV2.GetProperties() + + 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), currentRound) +} + +func TestThrowErrorIfTimeoutMsgRoundNotEqualToCurrentRound(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) + 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()) +} diff --git a/tests/consensus/v2_test.go b/tests/consensus/vote_test.go similarity index 68% rename from tests/consensus/v2_test.go rename to tests/consensus/vote_test.go index 4bac28454b..0932553bc7 100644 --- a/tests/consensus/v2_test.go +++ b/tests/consensus/vote_test.go @@ -11,106 +11,11 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCountdownTimeoutToSendTimeoutMessage(t *testing.T) { - blockchain, _, _, _ := PrepareXDCTestBlockChain(t, 11, params.TestXDPoSMockChainConfigWithV2Engine) - engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2 - - engineV2.SetNewRoundFaker(utils.Round(1), true) - - timeoutMsg := <-engineV2.BroadcastCh - assert.NotNil(t, timeoutMsg) - - valid, err := engineV2.VerifyTimeoutMessage(timeoutMsg.(*utils.Timeout)) - // We can only test valid = false for now as the implementation for getCurrentRoundMasterNodes is not complete - assert.False(t, valid) - // 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()) -} - -// 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) - currentRound, _, _ := engineV2.GetProperties() - assert.Equal(t, utils.Round(1), currentRound) - timeoutMsg = &utils.Timeout{ - Round: utils.Round(1), - Signature: []byte{2}, - } - err = engineV2.TimeoutHandler(timeoutMsg) - assert.Nil(t, err) - currentRound, _, _ = engineV2.GetProperties() - assert.Equal(t, utils.Round(1), currentRound) - // 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 - - currentRound, _, _ = engineV2.GetProperties() - - 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), currentRound) -} - -func TestThrowErrorIfTimeoutMsgRoundNotEqualToCurrentRound(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) - 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 TestVoteMessageHandlerSuccessfullyGeneratedAndProcessQC(t *testing.T) { blockchain, _, currentBlock, _ := PrepareXDCTestBlockChainForV2Engine(t, 11, params.TestXDPoSMockChainConfigWithV2Engine) engineV2 := blockchain.Engine().(*XDPoS.XDPoS).EngineV2 - // parentBlock := blockchain.GetBlockByHash(currentBlock.ParentHash()) - // grandParentBlock := blockchain.GetBlockByHash(parentBlock.ParentHash()) - blockInfo := &utils.BlockInfo{ Hash: currentBlock.Hash(), Round: utils.Round(11),