diff --git a/consensus/XDPoS/engines/engine_v2/engine.go b/consensus/XDPoS/engines/engine_v2/engine.go index 75c29b802a..05b920200b 100644 --- a/consensus/XDPoS/engines/engine_v2/engine.go +++ b/consensus/XDPoS/engines/engine_v2/engine.go @@ -2,7 +2,6 @@ package engine_v2 import ( "encoding/json" - "errors" "fmt" "io/ioutil" "math/big" @@ -200,6 +199,35 @@ func (x *XDPoS_v2) Initial(chain consensus.ChainReader, header *types.Header) er return nil } +// Check if it's my turm to mine a block. Note: The second return value `preIndex` is useless in V2 engine +func (x *XDPoS_v2) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (bool, error) { + x.lock.RLock() + defer x.lock.RUnlock() + + if !x.isInitilised { + err := x.Initial(chain, parent) + if err != nil { + log.Error("[YourTurn] Error while initialising last v2 variables", "ParentBlockHash", parent.Hash(), "Error", err) + return false, err + } + x.isInitilised = true + } + + waitedTime := time.Now().Unix() - parent.Time.Int64() + if waitedTime < int64(x.config.V2.MinePeriod) { + log.Trace("[YourTurn] wait after mine period", "minePeriod", x.config.V2.MinePeriod, "waitedTime", waitedTime) + return false, nil + } + + round := x.currentRound + isMyTurn, err := x.checkYourturnWithinFinalisedMasternodes(chain, round, parent, signer) + if err != nil { + log.Error("[Yourturn] Error while checking if i am qualified to mine", "round", round, "error", err) + } + + return isMyTurn, nil +} + // Prepare implements consensus.Engine, preparing all the consensus fields of the // header for running the transactions on top. func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) error { @@ -210,7 +238,6 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er x.lock.RUnlock() if header.ParentHash != highestQC.ProposedBlockInfo.Hash { - fmt.Println("[Prepare] parent hash and QC hash does not match", "blockNum", header.Number, "parentHash", header.ParentHash, "QCHash", highestQC.ProposedBlockInfo.Hash, "QCNumber", highestQC.ProposedBlockInfo.Number) log.Warn("[Prepare] parent hash and QC hash does not match", "blockNum", header.Number, "parentHash", header.ParentHash, "QCHash", highestQC.ProposedBlockInfo.Hash, "QCNumber", highestQC.ProposedBlockInfo.Number) return consensus.ErrNotReadyToPropose } @@ -230,13 +257,26 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er number := header.Number.Uint64() parent := chain.GetHeader(header.ParentHash, number-1) + log.Info("Preparing new block!", "Number", number, "Parent Hash", parent.Hash()) if parent == nil { return consensus.ErrUnknownAncestor } + x.signLock.RLock() + signer := x.signer + x.signLock.RUnlock() + + isMyTurn, err := x.checkYourturnWithinFinalisedMasternodes(chain, currentRound, parent, signer) + if err != nil { + log.Error("[Prepare] Error while checking if it's still my turn to mine", "round", currentRound, "ParentHash", parent.Hash().Hex(), "ParentNumber", parent.Number.Uint64(), "error", err) + return err + } + if !isMyTurn { + return consensus.ErrNotReadyToMine + } // Set the correct difficulty - header.Difficulty = x.calcDifficulty(chain, parent, x.signer) + header.Difficulty = x.calcDifficulty(chain, parent, signer) log.Debug("CalcDifficulty ", "number", header.Number, "difficulty", header.Difficulty) isEpochSwitchBlock, _, err := x.IsEpochSwitch(header) @@ -268,10 +308,6 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er header.Time = big.NewInt(time.Now().Unix()) } - x.signLock.RLock() - signer := x.signer - x.signLock.RUnlock() - if header.Coinbase != signer { log.Error("[Prepare] The mined blocker header coinbase address mismatch with waller address", "headerCoinbase", header.Coinbase.Hex(), "WalletAddress", signer.Hex()) return consensus.ErrCoinbaseMismatch @@ -372,77 +408,6 @@ func (x *XDPoS_v2) calcDifficulty(chain consensus.ChainReader, parent *types.Hea return big.NewInt(1) } -// Check if it's my turm to mine a block. Note: The second return value `preIndex` is useless in V2 engine -func (x *XDPoS_v2) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (bool, error) { - x.lock.RLock() - defer x.lock.RUnlock() - - if !x.isInitilised { - err := x.Initial(chain, parent) - if err != nil { - log.Error("[YourTurn] Error while initialising last v2 variables", "ParentBlockHash", parent.Hash(), "Error", err) - return false, err - } - x.isInitilised = true - } - - waitedTime := time.Now().Unix() - parent.Time.Int64() - if waitedTime < int64(x.config.V2.MinePeriod) { - log.Trace("[YourTurn] wait after mine period", "minePeriod", x.config.V2.MinePeriod, "waitedTime", waitedTime) - return false, nil - } - - round := x.currentRound - isEpochSwitch, _, err := x.isEpochSwitchAtRound(round, parent) - if err != nil { - log.Error("[YourTurn] check epoch switch at round failed", "Error", err) - return false, err - } - var masterNodes []common.Address - if isEpochSwitch { - if x.config.V2.SwitchBlock.Cmp(parent.Number) == 0 { - // the initial master nodes of v1->v2 switch contains penalties node - _, _, masterNodes, err = x.getExtraFields(parent) - if err != nil { - log.Error("[YourTurn] Cannot find snapshot at gap num of last V1", "err", err, "number", x.config.V2.SwitchBlock.Uint64()) - return false, err - } - } else { - masterNodes, _, err = x.calcMasternodes(chain, big.NewInt(0).Add(parent.Number, big.NewInt(1)), parent.Hash()) - if err != nil { - log.Error("[YourTurn] Cannot calcMasternodes at gap num ", "err", err, "parent number", parent.Number) - return false, err - } - } - } else { - // this block and parent belong to the same epoch - masterNodes = x.GetMasternodes(chain, parent) - } - - if len(masterNodes) == 0 { - log.Error("[YourTurn] Fail to find any master nodes from current block round epoch", "Hash", parent.Hash(), "CurrentRound", round, "Number", parent.Number) - return false, errors.New("masternodes not found") - } - - curIndex := utils.Position(masterNodes, signer) - if curIndex == -1 { - log.Debug("[YourTurn] Not authorised signer", "MN", masterNodes, "Hash", parent.Hash(), "signer", signer) - return false, nil - } - - for i, s := range masterNodes { - log.Debug("[YourTurn] Masternode:", "index", i, "address", s.String(), "parentBlockNum", parent.Number) - } - - leaderIndex := uint64(round) % x.config.Epoch % uint64(len(masterNodes)) - if masterNodes[leaderIndex] != signer { - log.Debug("[YourTurn] Not my turn", "curIndex", curIndex, "leaderIndex", leaderIndex, "Hash", parent.Hash(), "masterNodes[leaderIndex]", masterNodes[leaderIndex], "signer", signer) - return false, nil - } - - return true, nil -} - func (x *XDPoS_v2) IsAuthorisedAddress(chain consensus.ChainReader, header *types.Header, address common.Address) bool { x.lock.RLock() defer x.lock.RUnlock() diff --git a/consensus/XDPoS/engines/engine_v2/mining.go b/consensus/XDPoS/engines/engine_v2/mining.go new file mode 100644 index 0000000000..3ec944aefa --- /dev/null +++ b/consensus/XDPoS/engines/engine_v2/mining.go @@ -0,0 +1,65 @@ +package engine_v2 + +import ( + "errors" + "math/big" + + "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/log" +) + +// Using parent and current round to find the finalised master node list(with penalties applied from last epoch) +func (x *XDPoS_v2) checkYourturnWithinFinalisedMasternodes(chain consensus.ChainReader, round utils.Round, parent *types.Header, signer common.Address) (bool, error) { + isEpochSwitch, _, err := x.isEpochSwitchAtRound(round, parent) + if err != nil { + log.Error("[checkYourturnWithinFinalisedMasternodes] check epoch switch at round failed", "Error", err) + return false, err + } + var masterNodes []common.Address + if isEpochSwitch { + if x.config.V2.SwitchBlock.Cmp(parent.Number) == 0 { + // the initial master nodes of v1->v2 switch contains penalties node + _, _, masterNodes, err = x.getExtraFields(parent) + if err != nil { + log.Error("[checkYourturnWithinFinalisedMasternodes] Cannot find snapshot at gap num of last V1", "err", err, "number", x.config.V2.SwitchBlock.Uint64()) + return false, err + } + } else { + masterNodes, _, err = x.calcMasternodes(chain, big.NewInt(0).Add(parent.Number, big.NewInt(1)), parent.Hash()) + if err != nil { + log.Error("[checkYourturnWithinFinalisedMasternodes] Cannot calcMasternodes at gap num ", "err", err, "parent number", parent.Number) + return false, err + } + } + } else { + // this block and parent belong to the same epoch + masterNodes = x.GetMasternodes(chain, parent) + } + + if len(masterNodes) == 0 { + log.Error("[checkYourturnWithinFinalisedMasternodes] Fail to find any master nodes from current block round epoch", "Hash", parent.Hash(), "CurrentRound", round, "Number", parent.Number) + return false, errors.New("masternodes not found") + } + + curIndex := utils.Position(masterNodes, signer) + if curIndex == -1 { + log.Debug("[checkYourturnWithinFinalisedMasternodes] Not authorised signer", "MN", masterNodes, "Hash", parent.Hash(), "signer", signer) + return false, nil + } + + for i, s := range masterNodes { + log.Debug("[checkYourturnWithinFinalisedMasternodes] Masternode:", "index", i, "address", s.String(), "parentBlockNum", parent.Number) + } + + leaderIndex := uint64(round) % x.config.Epoch % uint64(len(masterNodes)) + if masterNodes[leaderIndex] != signer { + log.Debug("[checkYourturnWithinFinalisedMasternodes] Not my turn", "curIndex", curIndex, "leaderIndex", leaderIndex, "Hash", parent.Hash().Hex(), "masterNodes[leaderIndex]", masterNodes[leaderIndex], "signer", signer) + return false, nil + } + + log.Debug("[checkYourturnWithinFinalisedMasternodes] Yes, it's my turn based on parent block", "ParentHash", parent.Hash().Hex(), "ParentBlockNumber", parent.Number.Uint64()) + return true, nil +} diff --git a/consensus/errors.go b/consensus/errors.go index 03e3ed50ee..0747516f87 100644 --- a/consensus/errors.go +++ b/consensus/errors.go @@ -41,5 +41,7 @@ var ( ErrNotReadyToPropose = errors.New("not ready to propose, QC is not ready") + ErrNotReadyToMine = errors.New("Not ready to mine, it's not your turn") + ErrCoinbaseMismatch = errors.New("Block Coinbase address does not match its wallte address") ) diff --git a/consensus/tests/engine_v2_tests/mine_test.go b/consensus/tests/engine_v2_tests/mine_test.go index 17080d65c5..55ab880b8b 100644 --- a/consensus/tests/engine_v2_tests/mine_test.go +++ b/consensus/tests/engine_v2_tests/mine_test.go @@ -125,15 +125,38 @@ func TestUpdateMasterNodes(t *testing.T) { assert.Equal(t, int(snap.Number), 1350) } -func TestPrepare(t *testing.T) { +func TestPrepareFail(t *testing.T) { config := params.TestXDPoSMockChainConfig blockchain, _, currentBlock, signer, _, _ := PrepareXDCTestBlockChainForV2Engine(t, int(config.XDPoS.Epoch), config, 0) adaptor := blockchain.Engine().(*XDPoS.XDPoS) - _, err := adaptor.YourTurn(blockchain, currentBlock.Header(), common.HexToAddress("xdc0278C350152e15fa6FFC712a5A73D704Ce73E2E1")) - assert.Nil(t, err) tstamp := time.Now().Unix() + notReadyToProposeHeader := &types.Header{ + ParentHash: currentBlock.Hash(), + Number: big.NewInt(int64(901)), + GasLimit: params.TargetGasLimit, + Time: big.NewInt(tstamp), + Coinbase: signer, + } + + err := adaptor.Prepare(blockchain, notReadyToProposeHeader) + assert.Equal(t, consensus.ErrNotReadyToPropose, err) + + notReadyToMine := &types.Header{ + ParentHash: currentBlock.Hash(), + Number: big.NewInt(int64(901)), + GasLimit: params.TargetGasLimit, + Time: big.NewInt(tstamp), + Coinbase: signer, + } + // trigger initial which will set the highestQC + _, err = adaptor.YourTurn(blockchain, currentBlock.Header(), signer) + assert.Nil(t, err) + err = adaptor.Prepare(blockchain, notReadyToMine) + assert.Equal(t, consensus.ErrNotReadyToMine, err) + + adaptor.EngineV2.SetNewRoundFaker(blockchain, utils.Round(4), false) header901WithoutCoinbase := &types.Header{ ParentHash: currentBlock.Hash(), Number: big.NewInt(int64(901)), @@ -143,6 +166,17 @@ func TestPrepare(t *testing.T) { err = adaptor.Prepare(blockchain, header901WithoutCoinbase) assert.Equal(t, consensus.ErrCoinbaseMismatch, err) +} + +func TestPrepareHappyPath(t *testing.T) { + config := params.TestXDPoSMockChainConfig + blockchain, _, currentBlock, signer, _, _ := PrepareXDCTestBlockChainForV2Engine(t, int(config.XDPoS.Epoch), config, 0) + adaptor := blockchain.Engine().(*XDPoS.XDPoS) + // trigger initial + _, err := adaptor.YourTurn(blockchain, currentBlock.Header(), signer) + assert.Nil(t, err) + + tstamp := time.Now().Unix() header901 := &types.Header{ ParentHash: currentBlock.Hash(), @@ -152,6 +186,7 @@ func TestPrepare(t *testing.T) { Coinbase: signer, } + adaptor.EngineV2.SetNewRoundFaker(blockchain, utils.Round(4), false) err = adaptor.Prepare(blockchain, header901) assert.Nil(t, err) @@ -169,6 +204,6 @@ func TestPrepare(t *testing.T) { var decodedExtraField utils.ExtraFields_v2 err = utils.DecodeBytesExtraFields(header901.Extra, &decodedExtraField) assert.Nil(t, err) - assert.Equal(t, utils.Round(1), decodedExtraField.Round) + assert.Equal(t, utils.Round(4), decodedExtraField.Round) assert.Equal(t, utils.Round(0), decodedExtraField.QuorumCert.ProposedBlockInfo.Round) } diff --git a/consensus/tests/engine_v2_tests/penalty_test.go b/consensus/tests/engine_v2_tests/penalty_test.go index ef6bb7f202..a74c6c13b8 100644 --- a/consensus/tests/engine_v2_tests/penalty_test.go +++ b/consensus/tests/engine_v2_tests/penalty_test.go @@ -50,6 +50,9 @@ func TestHookPenaltyV2Mining(t *testing.T) { Time: header6300.Time, Coinbase: signer, } + // Force to make the node to be at its round to mine, otherwise won't pass the yourturn masternodes check + // We have 5 nodes in total and the node signer is always at the 4th(last) in the list. Hence int(config.XDPoS.Epoch)*7+4-900, the +4 means is to force to next 4 round and -900 is the relative round number to block number int(config.XDPoS.Epoch)*7 + adaptor.EngineV2.SetNewRoundFaker(blockchain, utils.Round(int(config.XDPoS.Epoch)*7+4-900), false) err = adaptor.Prepare(blockchain, headerMining) assert.Nil(t, err) assert.Equal(t, 1, len(headerMining.Penalties)/common.AddressLength)